适用于 PowerShell 5.1 及以上版本,并行功能需要 PowerShell 7
运维脚本经常面临 IO 等待——网络请求、文件操作、数据库查询,这些操作的响应时间往往远超 CPU 处理时间。如果顺序执行 100 个 HTTP 健康检查,每个耗时 1 秒,总共需要 100 秒;但如果并发执行,可能只需要几秒。PowerShell 提供了多种异步编程机制:后台作业(Jobs)、运行空间(Runspaces)、ForEach-Object -Parallel、以及 .NET 的异步 API。
本文将讲解 PowerShell 中的异步编程模式及其适用场景。
后台作业(Jobs) 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 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 $job = Start-Job -ScriptBlock { Start-Sleep -Seconds 2 Get-Process | Select-Object Name, Id, CPU | Sort-Object CPU -Descending | Select-Object -First 5 } Write-Host "作业已启动,ID:$ ($job .Id)" -ForegroundColor Cyanwhile ($job .State -eq 'Running' ) { Write-Host "." -NoNewline Start-Sleep -Milliseconds 500 } Write-Host $result = Receive-Job -Job $job Write-Host "作业完成,结果:" $result | Format-Table -AutoSize Remove-Job -Job $job $servers = @ ("SRV01" , "SRV02" , "SRV03" , "SRV04" , "SRV05" )$jobs = foreach ($server in $servers ) { Start-Job -ScriptBlock { param ($computer ) $os = Get-CimInstance Win32_OperatingSystem -ComputerName $computer -ErrorAction SilentlyContinue if ($os ) { [PSCustomObject ]@ { Server = $computer OS = $os .Caption Version = $os .Version Status = "Online" } } else { [PSCustomObject ]@ { Server = $computer OS = "N/A" Version = "N/A" Status = "Offline" } } } -ArgumentList $server } Write-Host "启动了 $ ($jobs .Count) 个后台作业" -ForegroundColor Cyan$jobs | Wait-Job | Out-Null $allResults = $jobs | Receive-Job $allResults | Format-Table -AutoSize $jobs | Remove-Job
执行结果示例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 . . . ---- -- --- . . . . . ------ -- ------- ------ . . . . . . . .
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 34 35 36 37 38 39 40 41 42 43 44 45 $servers = @ ("SRV01" , "SRV02" , "SRV03" , "SRV04" , "SRV05" , "SRV06" , "SRV07" , "SRV08" )$sw = [System.Diagnostics.Stopwatch ]::StartNew()$results = $servers | ForEach-Object -Parallel { $server = $_ try { $response = Invoke-WebRequest -Uri "http://$ {server}:8080/health" ` -TimeoutSec 5 -UseBasicParsing -ErrorAction Stop [PSCustomObject ]@ { Server = $server Status = "Healthy" Code = $response .StatusCode LatencyMs = $response .RawContentLength } } catch { [PSCustomObject ]@ { Server = $server Status = "Unhealthy" Code = 0 LatencyMs = 0 Error = $_ .Exception.Message } } } -ThrottleLimit 4 $sw .Stop()$results | Format-Table -AutoSize Write-Host "并行检查 8 台服务器耗时:$ ($sw .ElapsedMilliseconds)ms" -ForegroundColor Green$threshold = 80 $results = $servers | ForEach-Object -Parallel { $server = $_ $cpu = Get-Random -Min 10 -Max 95 [PSCustomObject ]@ { Server = $server CPU = $cpu IsAlert = $cpu -gt $using:threshold } } -ThrottleLimit 4 $results | Where-Object { $_ .IsAlert } | Format-Table -AutoSize
执行结果示例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 ------ ------ ---- --------- ------ --- -------
Runspace 池 对于需要精确控制的并发场景,使用 Runspace 池:
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 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 function Invoke-Parallel { param ( [Parameter (Mandatory )] [scriptblock ]$ScriptBlock , [Parameter (Mandatory )] [object []]$InputData , [int ]$ThrottleLimit = 4 ) $pool = [System.Management.Automation.Runspaces.RunspaceFactory ]::CreateRunspacePool( 1 , $ThrottleLimit ) $pool .Open() $runspaces = @ () foreach ($item in $InputData ) { $powershell = [System.Management.Automation.PowerShell ]::Create() $powershell .RunspacePool = $pool $null = $powershell .AddScript($ScriptBlock ).AddArgument($item ) $runspaces += [PSCustomObject ]@ { Pipe = $powershell Handle = $powershell .BeginInvoke() Input = $item } } $results = @ () foreach ($rs in $runspaces ) { try { $output = $rs .Pipe.EndInvoke($rs .Handle) if ($output ) { foreach ($o in $output ) { $results += $o } } } catch { $results += [PSCustomObject ]@ { Error = $true Input = $rs .Input Message = $_ .Exception.Message } } finally { $rs .Pipe.Dispose() } } $pool .Close() $pool .Dispose() return $results } $servers = 1 ..20 | ForEach-Object { "192.168.1.$_ " }$sw = [System.Diagnostics.Stopwatch ]::StartNew()$results = Invoke-Parallel -ScriptBlock { param ($ip ) $ping = Test-Connection -ComputerName $ip -Count 1 -Quiet -ErrorAction SilentlyContinue [PSCustomObject ]@ { IP = $ip ; Online = $ping } } -InputData $servers -ThrottleLimit 10 $sw .Stop()$online = ($results | Where-Object { $_ .Online }).CountWrite-Host "扫描 20 个 IP,$online 个在线,耗时:$ ($sw .ElapsedMilliseconds)ms" -ForegroundColor Green$results | Where-Object { $_ .Online } | Format-Table -AutoSize
执行结果示例:
1 2 3 4 5 6 7 8 9 10 11 12 扫描 20 个 IP,8 个在线,耗时:3200ms IP Online -- ------ 192.168.1.1 True192.168.1.2 True192.168.1.10 True192.168.1.11 True192.168.1.12 True192.168.1.20 True192.168.1.50 True192.168.1.100 True
异步文件操作 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 $files = Get-ChildItem "C:\Logs" -Filter "*.log" -Recurse | Select-Object -First 20 $sw = [System.Diagnostics.Stopwatch ]::StartNew()$fileStats = $files | ForEach-Object -Parallel { $file = $_ $lines = 0 $errors = 0 $reader = [System.IO.StreamReader ]::new($file .FullName) while ($null -ne $reader .ReadLine()) { $lines ++ } $reader .Close() [PSCustomObject ]@ { Name = $file .Name SizeKB = [math ]::Round($file .Length / 1 KB, 1 ) Lines = $lines Modified = $file .LastWriteTime.ToString('yyyy-MM-dd HH:mm' ) } } -ThrottleLimit 4 $sw .Stop()$fileStats | Format-Table -AutoSize Write-Host "并行处理 $ ($files .Count) 个文件耗时:$ ($sw .ElapsedMilliseconds)ms" -ForegroundColor Green
执行结果示例:
1 2 3 4 5 6 7 8 Name SizeKB Lines Modified ---- ------- ----- -------- app-20250709 .log 1024 .5 12050 2025-07-09 08 :30 app-20250708 .log 856 .3 9876 2025 -07-08 23:59 app-20250707 .log 743 .1 8543 2025 -07-07 23:59 security-20250709 .log 456 .7 5432 2025 -07-09 08:15 并行处理 20 个文件耗时:850m s
注意事项
Jobs 开销大 :每个 Job 启动新的 PowerShell 进程,开销较大。少量任务用 Jobs,大量任务用 Runspace 或 -Parallel
线程安全 :并行操作中不要直接修改共享变量,使用 ConcurrentDictionary 或收集结果后统一处理
ThrottleLimit :设置合理的并发限制,过多并发会导致资源争用和 API 限流
错误处理 :并行任务中的错误不会自动传播到主线程,需要显式捕获和收集
$using: 作用域 :ForEach-Object -Parallel 中访问外部变量需要 $using: 前缀
Runspace 清理 :使用完 Runspace 和 RunspacePool 后必须调用 Dispose() 释放资源