Foreach Parallel を使用した複数のスレッド間での進行状況の書き込み
PowerShell 7.0 以降では、Foreach-Object コマンドレットで Parallel パラメーターを使用して、複数のスレッドで同時に作業できるようになります。 ただし、これらのスレッドの進行状況を監視することは困難な場合があります。 通常は、Write-Progress を使用してプロセスの進行状況を監視できます。
ただし、PowerShell では Parallel を使用するときにスレッドごとに個別の実行空間が使用されるため、進行状況をホストに報告することは、Write-Progress
の通常の使用時ほど簡単ではありません。
同期されたハッシュテーブルを使用した進行状況の追跡
複数のスレッドの進行状況を書き込む場合、PowerShell で並列プロセスを実行すると、各プロセスで独自の実行空間が使用されるため、追跡が困難になります。 この問題を回避するには、同期されたハッシュテーブルを使用できます。 同期されたハッシュテーブルは、スレッド セーフなデータ構造であり、エラーをスローすることなく複数のスレッドによって同時に変更できます。
設定
このアプローチの欠点の 1 つは、すべてがエラーなしで確実に実行されるように、やや複雑な設定となることです。
$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)
このセクションでは、3 つの異なる目的で、3 つの異なるデータ構造を作成します。
$dataSet
変数には、変更されるリスクを伴わずに次の手順を調整するために使用されるハッシュテーブルの配列が格納されます。 コレクションの反復処理中にオブジェクト コレクションが変更された場合、PowerShell によってエラーがスローされます。 オブジェクト コレクションは、変更対象のオブジェクトとは別のループに保持する必要があります。 各ハッシュテーブルの Id
キーは、モック プロセスの識別子です。 Wait
キーにより、追跡されている各モック プロセスのワークロードがシミュレートされます。
$origin
変数には、各キーがモック プロセス ID の 1 つである、入れ子になったハッシュテーブルが格納されます。
次に、これは $sync
変数に格納されている同期済みのハッシュテーブルをハイドレートするために使用されます。 $sync
変数は、進行状況が表示される親実行空間に進行状況を報告する役割を担います。
プロセスの実行
このセクションでは、マルチスレッド プロセスを実行し、進行状況を表示するために使用される出力の一部を作成します。
$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 = $using:sync.$($PSItem.id)
のような $process
変数を作成しようとすると、ここで式を使用できないことを示すエラーが表示されます。 そのため、エラーが表示されることなく $sync
変数を参照および変更できるように、$syncCopy
変数を作成します。
次に、同期されたハッシュテーブル キーを参照することで、$process
変数を使用して現在ループ内にあるプロセスの進行状況を表すハッシュテーブルを作成します。 Activity および Status キーは、次のセクションで特定のモック プロセスの状態を表示するために Write-Progress
のパラメーター値として使用されます。
foreach
ループは、プロセスの動作をシミュレートする 1 つの方法であり、$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
変数には親ジョブが含まれており、モック プロセスごとに子ジョブが存在します。 子ジョブがまだ実行されている間、親ジョブの State は "Running" のままになります。 これにより、すべてのプロセスが完了するまで、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
}
関連リンク
PowerShell