在多個線程中使用
從 PowerShell 7.0 開始,可以使用 ForEach-Object Cmdlet 中的 Parallel 參數,同時在多個線程中工作。 不過,監視這些線程的進度可能是一項挑戰。 一般而言,您可以使用 Write-Progress監視進程的進度。
不過,由於 PowerShell 在使用 Parallel時會為每個線程使用不同的 Runspace,因此向主機回報進度並不像正常使用 Write-Progress
時那樣簡單明瞭。
使用同步哈希表來追蹤進度
從多個線程撰寫進度時,追蹤會變得困難,因為在PowerShell中執行平行進程時,每個進程都有自己的 Runspace。 若要解決此問題,您可以使用 同步化哈希表。 同步哈希表是線程安全的數據結構,可同時由多個線程修改,而不會擲回錯誤。
設定
這種方法的其中一個缺點是,它需要一個有點複雜設定,以確保一切執行時不會發生錯誤。
$dataset = @(
@{
Id = 1
Wait = 3..10 | Get-Random | ForEach-Object {$_*100}
}
@{
Id = 2
Wait = 3..10 | Get-Random | ForEach-Object {$_*100}
}
@{
Id = 3
Wait = 3..10 | Get-Random | ForEach-Object {$_*100}
}
@{
Id = 4
Wait = 3..10 | Get-Random | ForEach-Object {$_*100}
}
@{
Id = 5
Wait = 3..10 | Get-Random | ForEach-Object {$_*100}
}
)
# Create a hashtable for process.
# Keys should be ID's of the processes
$origin = @{}
$dataset | ForEach-Object {$origin.($_.Id) = @{}}
# Create synced hashtable
$sync = [System.Collections.Hashtable]::Synchronized($origin)
本節會建立三個不同的數據結構,以供三個不同的用途使用。
$dataSet
變數會存儲一個包含多個哈希表的陣列,用來協調後續步驟,且不會被修改。 如果在逐一查看集合時修改物件集合,PowerShell 會擲回錯誤。 您必須將物件集合保留在迴圈中,與正在修改的物件分開。 每個哈希表中 Id
索引鍵是模擬進程的標識碼。
Wait
索引鍵會模擬所追蹤之每個模擬程式的工作負載。
$origin
變數會儲存巢狀哈希表,其中每個索引鍵都是模擬進程標識碼的其中一個。
然後,它會用來凍結儲存在 $sync
變數中的同步哈希表。
$sync
變數負責將進度回報回父 Runspace,以顯示進度。
執行進程
本節會執行多線程進程,並建立一些用來顯示進度的輸出。
$job = $dataset | ForEach-Object -ThrottleLimit 3 -AsJob -Parallel {
$syncCopy = $Using:sync
$process = $syncCopy.$($PSItem.Id)
$process.Id = $PSItem.Id
$process.Activity = "Id $($PSItem.Id) starting"
$process.Status = "Processing"
# Fake workload start up that takes x amount of time to complete
Start-Sleep -Milliseconds ($PSItem.Wait*5)
# Process. update activity
$process.Activity = "Id $($PSItem.Id) processing"
foreach ($percent in 1..100)
{
# Update process on status
$process.Status = "Handling $percent/100"
$process.PercentComplete = (($percent / 100) * 100)
# Fake workload that takes x amount of time to complete
Start-Sleep -Milliseconds $PSItem.Wait
}
# Mark process as completed
$process.Completed = $true
}
模擬過程會傳送至 ForEach-Object
,並啟動為任務。
ThrottleLimit 設定為 3,以強調在佇列中執行多個程序。 作業會儲存在 $job
變數中,並允許我們知道所有進程稍後完成的時間。
使用 Using:
語句在 PowerShell 中參考父範圍變數時,您無法使用表達式讓它成為動態。 例如,如果您嘗試建立類似下列的 $process
變數,$process = $Using:sync.$($PSItem.Id)
,您會收到錯誤,指出您無法在該處使用表達式。 因此,我們會建立 $syncCopy
變數,以便參考和修改 $sync
變數,而不會造成失敗的風險。
接下來,我們會建置一個雜湊表,以 $process
變數來參考同步的雜湊表索引鍵,從而表示目前在迴圈中的流程進度。
活動鍵 和 狀態索引鍵 作為參數供 Write-Progress
使用,以便在下一節中顯示指定模擬過程的狀態。
foreach
迴圈只是模擬進程運作的一種方式,並根據 $dataSet
Wait 屬性隨機化,以使用毫秒設定 Start-Sleep
。 您計算程序進度的方式可能會有所不同。
顯示多個進程的進度
現在模擬進程會以作業的形式執行,我們可以開始將進程進度寫入 PowerShell 視窗。
while($job.State -eq 'Running')
{
$sync.Keys | ForEach-Object {
# If key is not defined, ignore
if(![string]::IsNullOrEmpty($sync.$_.Keys))
{
# Create parameter hashtable to splat
$param = $sync.$_
# Execute Write-Progress
Write-Progress @param
}
}
# Wait to refresh to not overload gui
Start-Sleep -Seconds 0.1
}
$job
變數包含父 作業,而且每個模擬進程都有子 作業。 當任何子作業仍在執行時,父作業 狀態 仍會維持「執行中」。 這可讓我們使用 while
循環,持續更新每個進程的進度,直到所有進程都完成為止。
在 while 循環內,我們會遍歷 $sync
變數中的每個鍵。 由於這是同步哈希表,因此會持續更新,但仍可存取,且不會發生任何錯誤。
有一項檢查可確保所報告的進程實際上是使用 IsNullOrEmpty()
方法執行。 如果進程尚未啟動,迴圈將不會報告此進程,並會跳過到下一個,直到遇到一個已啟動的進程為止。 如果程序已啟動,則會使用當前鍵中的哈希表來展開參數到 Write-Progress
。
完整範例
# Example workload
$dataset = @(
@{
Id = 1
Wait = 3..10 | Get-Random | ForEach-Object {$_*100}
}
@{
Id = 2
Wait = 3..10 | Get-Random | ForEach-Object {$_*100}
}
@{
Id = 3
Wait = 3..10 | Get-Random | ForEach-Object {$_*100}
}
@{
Id = 4
Wait = 3..10 | Get-Random | ForEach-Object {$_*100}
}
@{
Id = 5
Wait = 3..10 | Get-Random | ForEach-Object {$_*100}
}
)
# Create a hashtable for process.
# Keys should be ID's of the processes
$origin = @{}
$dataset | ForEach-Object {$origin.($_.Id) = @{}}
# Create synced hashtable
$sync = [System.Collections.Hashtable]::Synchronized($origin)
$job = $dataset | ForEach-Object -ThrottleLimit 3 -AsJob -Parallel {
$syncCopy = $Using:sync
$process = $syncCopy.$($PSItem.Id)
$process.Id = $PSItem.Id
$process.Activity = "Id $($PSItem.Id) starting"
$process.Status = "Processing"
# Fake workload start up that takes x amount of time to complete
Start-Sleep -Milliseconds ($PSItem.Wait*5)
# Process. update activity
$process.Activity = "Id $($PSItem.Id) processing"
foreach ($percent in 1..100)
{
# Update process on status
$process.Status = "Handling $percent/100"
$process.PercentComplete = (($percent / 100) * 100)
# Fake workload that takes x amount of time to complete
Start-Sleep -Milliseconds $PSItem.Wait
}
# Mark process as completed
$process.Completed = $true
}
while($job.State -eq 'Running')
{
$sync.Keys | ForEach-Object {
# If key is not defined, ignore
if(![string]::IsNullOrEmpty($sync.$_.Keys))
{
# Create parameter hashtable to splat
$param = $sync.$_
# Execute Write-Progress
Write-Progress @param
}
}
# Wait to refresh to not overload gui
Start-Sleep -Seconds 0.1
}