PowerShell 提供數個選項來建立平行調用。
-
Start-Job在不同的進程中執行每個作業,每個作業都有新的PowerShell實例。 在許多情況下,線性循環的速度較快。 此外,串行化和還原串行化可以限制所傳回物件的有用性。 此命令內建於所有 PowerShell 版本。 -
Start-ThreadJob是 ThreadJob 模組中找到的 Cmdlet。 此命令會使用PowerShell Runspaces來建立和管理線程型作業。 這些作業比由Start-Job建立的作業更輕,可避免跨進程串行化和還原串行化所需的類型保真度可能遺失。 ThreadJob 模組隨附 PowerShell 7 和更新版本。 針對 Windows PowerShell 5.1,您可以從 PowerShell 資源庫安裝此模組。 - 使用 PowerShell SDK 中的 System.Management.Automation.Runspaces 命名空間來建立您自己的平行邏輯。 和
ForEach-Object -Parallel及Start-ThreadJob均使用 PowerShell 執行空間來平行執行程式碼。 - 工作流程是 Windows PowerShell 5.1 的功能。 PowerShell 7.0 及更高版本中無法使用工作流程。 工作流程是一種特殊的 PowerShell 腳本類型,可平行執行。 它們是針對長時間執行的工作所設計,而且可以暫停和繼續。 不建議在新的開發項目中使用工作流程。 如需詳細資訊,請參閱 about_Workflows。
-
ForEach-Object -Parallel是 PowerShell 7.0 和更新版本的功能。 如同Start-ThreadJob,它會使用PowerShell Runspaces來建立和管理線程型作業。 此命令的設計目的是在管線中使用。
限制執行的同時性
平行執行腳本並不保證效能改善。 例如,下列案例可受益於平行執行:
- 在多線程、多核心處理器上計算密集型腳本
- 執行的腳本會花時間等候結果或執行檔案作業,只要這些作業不會互相封鎖。
請務必平衡平行執行的額外負荷與完成的工作類型。 此外,可以平行執行的調用數目也有限制。
Start-ThreadJob和 ForEach-Object -Parallel 命令具有 ThrottleLimit 參數,可限制一次執行的作業數目。 隨著更多作業的啟動,它們會排入佇列,並等到目前的作業數目低於節流限制為止。 從 PowerShell 7.1 開始, ForEach-Object -Parallel 預設會重複使用 Runspace 集區中的 Runspace。
ThrottleLimit 參數會設定 Runspace 集區大小。 預設 Runspace 集區大小為 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 在此 Stack Overflow 文章中的答案。
你可能也會對 Santiago Squarzon 開發的 PSParallelPipeline 模組感興趣。