PowerShell 提供了多个用于创建并行调用的选项。
-
Start-Job
在单独的进程中运行每个作业,每个作业都有一个新的 PowerShell 实例。 在许多情况下,线性循环速度更快。 此外,序列化和反序列化可以限制返回的对象的有用性。 此命令内置于所有版本的 PowerShell 中。 -
Start-ThreadJob
是在 ThreadJob 模块中找到的 cmdlet。 此命令使用 PowerShell 运行空间来创建和管理基于线程的作业。 这些作业的权重比创建Start-Job
作业轻,并避免跨进程序列化和反序列化所需的类型保真度可能丢失。 ThreadJob 模块附带 PowerShell 7 及更高版本。 对于 Windows PowerShell 5.1,可以从 PowerShell 库安装此模块。 - 使用 PowerShell SDK 中的 System.Management.Automation.Runspaces 命名空间创建自己的并行逻辑。 同时使用
ForEach-Object -Parallel
Start-ThreadJob
PowerShell Runspaces 并行执行代码。 - 工作流是 Windows PowerShell 5.1 的一项功能。 工作流在 PowerShell 7.0 及更高版本中不可用。 工作流是一种特殊类型的 PowerShell 脚本,可以并行运行。 它们专为长时间运行的任务而设计,可以暂停和恢复。 不建议使用工作流进行新开发。 有关详细信息,请参阅 about_Workflows。
-
ForEach-Object -Parallel
是 PowerShell 7.0 及更高版本的一项功能。 同样Start-ThreadJob
,它使用 PowerShell 运行空间来创建和管理基于线程的作业。 此命令旨在用于管道。
限制执行并发
并行运行脚本不能保证提高性能。 例如,以下方案可以从并行执行中受益:
- 在多线程多核处理器上计算密集型脚本
- 只要这些作不相互阻止,等待结果或执行文件作的脚本。
必须平衡并行执行的开销以及完成的工作类型。 此外,可以并行运行的调用数有限制。
命令Start-ThreadJob
ForEach-Object -Parallel
具有一个 ThrottleLimit 参数,用于限制一次运行的作业数。 随着更多作业的启动,它们已排队,等待当前作业数低于限制限制。 从 PowerShell 7.1 起,默认情况下会 ForEach-Object -Parallel
重复使用 Runspace 池中的运行空间。
ThrottleLimit 参数设置运行空间池大小。 默认的运行空间池大小为 5。 仍然可以使用 UseNewRunspace 开关为每个迭代创建新的运行空间。
该 Start-Job
命令没有 ThrottleLimit 参数。 必须一次管理正在运行的作业数。
度量性能
以下函数 Measure-Parallel
比较以下并行执行方法的速度:
Start-Job
- 在后台创建子 PowerShell 进程Start-ThreadJob
- 在单独的线程中运行每个作业ForEach-Object -Parallel
- 在单独的线程中运行每个作业Start-Process
- 异步调用外部程序注释
仅当并行任务仅包含对外部程序的单个调用(而不是运行 PowerShell 代码块)时,此方法才有意义。 此外,使用此方法捕获输出的唯一方法是重定向到文件。
function Measure-Parallel {
[CmdletBinding()]
param(
[ValidateRange(2, 2147483647)]
[int] $BatchSize = 5,
[ValidateSet('Job', 'ThreadJob', 'Process', 'ForEachParallel', 'All')]
[string[]] $Approach,
# pass a higher count to run multiple batches
[ValidateRange(2, 2147483647)]
[int] $JobCount = $BatchSize
)
$noForEachParallel = $PSVersionTable.PSVersion.Major -lt 7
$noStartThreadJob = -not (Get-Command -ErrorAction Ignore Start-ThreadJob)
# Translate the approach arguments into their corresponding hashtable keys (see below).
if ('All' -eq $Approach) { $Approach = 'Job', 'ThreadJob', 'Process', 'ForEachParallel' }
$approaches = $Approach.ForEach({
if ($_ -eq 'ForEachParallel') { 'ForEach-Object -Parallel' }
else { $_ -replace '^', 'Start-' }
})
if ($noStartThreadJob) {
if ($interactive -or $approaches -contains 'Start-ThreadJob') {
Write-Warning "Start-ThreadJob is not installed, omitting its test."
$approaches = $approaches.Where({ $_ -ne 'Start-ThreadJob' })
}
}
if ($noForEachParallel) {
if ($interactive -or $approaches -contains 'ForEach-Object -Parallel') {
Write-Warning 'ForEach-Object -Parallel require PowerShell v7+, omitting its test.'
$approaches = $approaches.Where({ $_ -ne 'ForEach-Object -Parallel' })
}
}
# Simulated input: Create 'f0.zip', 'f1'.zip', ... file names.
$zipFiles = 0..($JobCount - 1) -replace '^', 'f' -replace '$', '.zip'
# Sample executables to run - here, the native shell is called to simply
# echo the argument given.
$exe = if ($env:OS -eq 'Windows_NT') { 'cmd.exe' } else { 'sh' }
# The list of its arguments *as a single string* - use '{0}' as the placeholder
# for where the input object should go.
$exeArgList = if ($env:OS -eq 'Windows_NT') {
'/c "echo {0} > NUL:"'
} else {
'-c "echo {0} > /dev/null"'
}
# A hashtable with script blocks that implement the 3 approaches to parallelism.
$approachImpl = [ordered] @{}
# child-process-based job
$approachImpl['Start-Job'] = {
param([array] $batch)
$batch |
ForEach-Object {
Start-Job {
Invoke-Expression ($using:exe + ' ' + ($using:exeArgList -f $args[0]))
} -ArgumentList $_
} |
Receive-Job -Wait -AutoRemoveJob | Out-Null
}
# thread-based job - requires the ThreadJob module
if (-not $noStartThreadJob) {
# If Start-ThreadJob is available, add an approach for it.
$approachImpl['Start-ThreadJob'] = {
param([array] $batch)
$batch |
ForEach-Object {
Start-ThreadJob -ThrottleLimit $BatchSize {
Invoke-Expression ($using:exe + ' ' + ($using:exeArgList -f $args[0]))
} -ArgumentList $_
} |
Receive-Job -Wait -AutoRemoveJob | Out-Null
}
}
# ForEach-Object -Parallel job
if (-not $noForEachParallel) {
$approachImpl['ForEach-Object -Parallel'] = {
param([array] $batch)
$batch | ForEach-Object -ThrottleLimit $BatchSize -Parallel {
Invoke-Expression ($using:exe + ' ' + ($using:exeArgList -f $_))
}
}
}
# direct execution of an external program
$approachImpl['Start-Process'] = {
param([array] $batch)
$batch |
ForEach-Object {
Start-Process -NoNewWindow -PassThru $exe -ArgumentList ($exeArgList -f $_)
} |
Wait-Process
}
# Partition the array of all indices into subarrays (batches)
$batches = @(
0..([math]::Ceiling($zipFiles.Count / $batchSize) - 1) | ForEach-Object {
, $zipFiles[($_ * $batchSize)..($_ * $batchSize + $batchSize - 1)]
}
)
$tsTotals = foreach ($appr in $approaches) {
$i = 0
$tsTotal = [timespan] 0
$batches | ForEach-Object {
Write-Verbose "$batchSize-element '$appr' batch"
$ts = Measure-Command { & $approachImpl[$appr] $_ | Out-Null }
$tsTotal += $ts
if (++$i -eq $batches.Count) {
# last batch processed.
if ($batches.Count -gt 1) {
Write-Verbose ("'$appr' processing $JobCount items finished in " +
"$($tsTotal.TotalSeconds.ToString('N2')) secs.")
}
$tsTotal # output the overall timing for this approach
}
}
}
# Output a result object with the overall timings.
$oht = [ordered] @{}
$oht['JobCount'] = $JobCount
$oht['BatchSize'] = $BatchSize
$oht['BatchCount'] = $batches.Count
$i = 0
foreach ($appr in $approaches) {
$oht[($appr + ' (secs.)')] = $tsTotals[$i++].TotalSeconds.ToString('N2')
}
[pscustomobject] $oht
}
以下示例使用 Measure-Parallel
所有可用方法并行运行 20 个作业,一次运行 5 个作业。
Measure-Parallel -Approach All -BatchSize 5 -JobCount 20 -Verbose
以下输出来自运行 PowerShell 7.5.1 的 Windows 计算机。 你的时间可能会因多种因素而异,但比率应提供相对性能感。
VERBOSE: 5-element 'Start-Job' batch
VERBOSE: 5-element 'Start-Job' batch
VERBOSE: 5-element 'Start-Job' batch
VERBOSE: 5-element 'Start-Job' batch
VERBOSE: 'Start-Job' processing 20 items finished in 7.58 secs.
VERBOSE: 5-element 'Start-ThreadJob' batch
VERBOSE: 5-element 'Start-ThreadJob' batch
VERBOSE: 5-element 'Start-ThreadJob' batch
VERBOSE: 5-element 'Start-ThreadJob' batch
VERBOSE: 'Start-ThreadJob' processing 20 items finished in 2.37 secs.
VERBOSE: 5-element 'Start-Process' batch
VERBOSE: 5-element 'Start-Process' batch
VERBOSE: 5-element 'Start-Process' batch
VERBOSE: 5-element 'Start-Process' batch
VERBOSE: 'Start-Process' processing 20 items finished in 0.26 secs.
VERBOSE: 5-element 'ForEach-Object -Parallel' batch
VERBOSE: 5-element 'ForEach-Object -Parallel' batch
VERBOSE: 5-element 'ForEach-Object -Parallel' batch
VERBOSE: 5-element 'ForEach-Object -Parallel' batch
VERBOSE: 'ForEach-Object -Parallel' processing 20 items finished in 0.79 secs.
JobCount : 20
BatchSize : 5
BatchCount : 4
Start-Job (secs.) : 7.58
Start-ThreadJob (secs.) : 2.37
Start-Process (secs.) : 0.26
ForEach-Object -Parallel (secs.) : 0.79
结论
- 此方法
Start-Process
性能最佳,因为它没有作业管理的开销。 但是,如前所述,此方法具有根本限制。 - 该
ForEach-Object -Parallel
开销将增加最少的开销,后跟Start-ThreadJob
。 -
Start-Job
由于它为每个作业创建的隐藏 PowerShell 实例,因此开销最大。
致谢
本文的大部分信息都基于圣地亚哥·斯夸尔松和堆栈溢出帖子中的 mklement0 的解答。
你可能还对圣地亚哥 Squarzon 创建的 PSParallelPipeline 模块感兴趣。