适用于 PowerShell 7.0 及以上版本
当我们需要对大量对象执行相同操作时,传统的顺序处理方式往往耗时过长。例如检查 100 台服务器的连通性,逐台 ping 可能需要几分钟,但如果同时发起多个 ping 操作,时间可以缩短到几秒钟。
PowerShell 7 引入了 ForEach-Object -Parallel 参数,让并行处理变得前所未有的简单。对于更高级的场景,还可以通过 Runspace 直接操控底层并发机制。本文将系统介绍 PowerShell 中的并行处理方法,帮助你在合适的场景下大幅提升脚本执行效率。
ForEach-Object -Parallel 基础 ForEach-Object -Parallel 是 PowerShell 7 新增的功能,它为管道中的每个元素创建一个独立的运行空间来并行执行脚本块。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 $servers = @ ( "server-01.vichamp.com" , "server-02.vichamp.com" , "server-03.vichamp.com" , "server-04.vichamp.com" , "server-05.vichamp.com" ) Measure-Command { $servers | ForEach-Object -Parallel { $result = Test-Connection -ComputerName $_ -Count 2 -Quiet [PSCustomObject ]@ { Server = $_ Online = $result } } | Format-Table -AutoSize } | Select-Object TotalSeconds
1 2 3 4 5 6 7 8 9 10 11 Server Onlineserver -01. vichamp.com True server -02. vichamp.com True server -03. vichamp.com False server -04. vichamp.com True server -05. vichamp.com True TotalSeconds 2.34
使用 $using: 传递变量 在 -Parallel 脚本块中,不能直接访问外部作用域的变量。需要使用 $using: 前缀来引用外部变量。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 $config = @ { Port = 443 Timeout = 5000 AlertTo = "admin@vichamp.com" } $servers | ForEach-Object -Parallel { $serverConfig = $using:config $tcpClient = [System.Net.Sockets.TcpClient ]::new() try { $asyncResult = $tcpClient .BeginConnect($_ , $serverConfig .Port, $null , $null ) $waited = $asyncResult .AsyncWaitHandle.WaitOne($serverConfig .Timeout, $false ) [PSCustomObject ]@ { Server = $_ Port = $serverConfig .Port Open = $tcpClient .Connected } } catch { [PSCustomObject ]@ { Server = $_ Port = $serverConfig .Port Open = $false } } finally { $tcpClient .Close() } } | Format-Table -AutoSize
1 2 3 4 5 6 7 Server Port Open server -01. vichamp.com 443 True server -02. vichamp.com 443 True server -03. vichamp.com 443 False server -04. vichamp.com 443 True server -05. vichamp.com 443 True
传递外部函数 如果需要在并行脚本块中调用自定义函数,也需要通过 $using: 传递,或者在脚本块内重新定义。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 function Get-ServerHealth { param ([string ]$ServerName ) $cpu = Get-Random -Minimum 10 -Maximum 95 $mem = Get-Random -Minimum 30 -Maximum 90 [PSCustomObject ]@ { Server = $ServerName CPU = "$cpu %" Memory = "$mem %" Status = if ($cpu -gt 85 -or $mem -gt 85 ) { "警告" } else { "正常" } } } $healthFunc = $ {function:Get-ServerHealth }$servers | ForEach-Object -Parallel { $ {function:Get-ServerHealth } = $using:healthFunc Get-ServerHealth -ServerName $_ } | Format-Table -AutoSize
并发限流 当目标数量很大时,不加限制的并行可能会耗尽系统资源。使用 -ThrottleLimit 参数可以控制最大并发数。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 $targets = 1 ..50 | ForEach-Object { "host-$ ($_ .ToString('00'))" }$results = $targets | ForEach-Object -ThrottleLimit 5 -Parallel { Start-Sleep -Milliseconds (Get-Random -Minimum 100 -Maximum 500 ) [PSCustomObject ]@ { Host = $_ Latency = "$ (Get-Random -Minimum 10 -Maximum 200)ms" Checked = (Get-Date ).ToString('HH:mm:ss.fff' ) } } $results | Format-Table -AutoSize
1 2 3 4 5 6 Host Latency Checked ---- ------- -------- host-01 87ms 09:15 :23.112 host-02 142ms 09:15 :23.234 host-03 35ms 09:15 :23.178 ...
动态调整并发数 对于不同类型的任务,最佳并发数不同。可以编写一个带参数的函数来灵活控制。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 function Invoke-BatchParallel { param ( [Parameter (Mandatory )] [array ]$Items , [scriptblock ]$ScriptBlock , [int ]$ThrottleLimit = 10 , [int ]$TimeoutSeconds = 300 ) $results = $Items | ForEach-Object -Parallel $ScriptBlock -ThrottleLimit $ThrottleLimit -AsJob $completed = $results | Wait-Job -Timeout $TimeoutSeconds $output = $completed | Receive-Job $results | Remove-Job -Force return $output } $serverList = @ ("SRV-01" , "SRV-02" , "SRV-03" , "SRV-04" , "SRV-05" )Invoke-BatchParallel -Items $serverList -ThrottleLimit 3 -ScriptBlock { Get-CimInstance -ClassName Win32_LogicalDisk -Filter "DriveType=3" -ComputerName $_ | Select-Object PSComputerName, DeviceID, @ {N='总空间(GB)' ;E={[math ]::Round($_ .Size/1 GB,2 )}}, @ {N='剩余(GB)' ;E={[math ]::Round($_ .FreeSpace/1 GB,2 )}}, @ {N='使用率' ;E={"$ ([math]::Round(($_ .Size - $_ .FreeSpace)/$_ .Size * 100, 1))%" }} } | Format-Table -AutoSize
使用 Runspace 实现高级并行 对于需要更精细控制的场景,可以直接使用 .NET 的 Runspace 来创建线程池。这种方式比 ForEach-Object -Parallel 更底层,但灵活性更高。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 $runspacePool = [runspacefactory ]::CreateRunspacePool(1 , 8 )$runspacePool .Open()$jobs = foreach ($server in $servers ) { $powershell = [powershell ]::Create().AddScript({ param ($ComputerName ) $os = Get-CimInstance -ClassName Win32_OperatingSystem -ComputerName $ComputerName [PSCustomObject ]@ { Server = $ComputerName OS = $os .Caption LastBoot = $os .LastBootUpTime UptimeDays = ((Get-Date ) - $os .LastBootUpTime).Days } }).AddArgument($server ) $powershell .RunspacePool = $runspacePool [PSCustomObject ]@ { Pipe = $powershell Handle = $powershell .BeginInvoke() } } foreach ($job in $jobs ) { $result = $job .Pipe.EndInvoke($job .Handle) $result $job .Pipe.Dispose() } $runspacePool .Close()$runspacePool .Dispose()
1 2 3 4 5 6 Server OS LastBoot UptimeDaysserver -01 Microsoft Windows Server 2022 2025 /3 /15 8 :00 :00 25 server -02 Microsoft Windows Server 2022 2025 /4 /1 10 :30 :00 8 server -04 Microsoft Windows Server 2019 2025 /2 /20 14 :00 :00 48 server -05 Microsoft Windows Server 2022 2025 /3 /28 6 :15 :00 12
注意事项
ForEach-Object -Parallel 是 PowerShell 7 的功能,在 Windows PowerShell 5.1 中不可用
并行脚本块中运行在独立的运行空间中,不能直接共享变量状态,需要使用 $using: 或其他机制传递数据
合理设置 -ThrottleLimit,一般不超过 CPU 核心数的 2-4 倍,避免上下文切换开销过大
并行处理会产生多个运行空间,内存消耗会比顺序处理更高
对于 I/O 密集型任务(网络请求、文件读写),并行效果最明显;对于 CPU 密集型任务,收益有限
并行执行时输出顺序不确定,如需保持顺序,请在结果中附加原始索引后排序
在并行脚本块中避免写入同一个文件,否则可能出现文件锁冲突