about_Thread_Jobs

簡単な説明

PowerShell スレッド ベースのジョブに関する情報を提供します。 スレッド ジョブは、現在のセッション プロセス内の別のスレッドでコマンドまたは式を実行するバックグラウンド ジョブの一種です。

詳細な説明

PowerShell は、ジョブを介してコマンドとスクリプトを同時に実行します。 同時実行をサポートするために、PowerShell によって提供されるジョブの種類は 3 つあります。

  • RemoteJob - コマンドとスクリプトはリモート セッションで実行されます。 詳細については、about_Remote_Jobsを参照してください
  • BackgroundJob - コマンドとスクリプトは、ローカル コンピューター上の別のプロセスで実行されます。 詳細については、「about_Jobs」を参照してください。
  • PSTaskJob または ThreadJob - コマンドとスクリプトは、ローカル コンピューター上の同じプロセス内の別のスレッドで実行されます。

スレッド ベースのジョブは、異なるスレッドで同じプロセスで実行されるため、リモート ジョブやバックグラウンド ジョブほど堅牢ではありません。 1 つのジョブでプロセスがクラッシュする重大なエラーが発生した場合、プロセス内の他のすべてのジョブが終了します。

ただし、スレッド ベースのジョブのオーバーヘッドは少なくなります。 リモート処理レイヤーやシリアル化は使用しません。 結果オブジェクトは、現在のセッションのライブ オブジェクトへの参照として返されます。 このオーバーヘッドがなければ、スレッドベースのジョブの実行速度が速くなり、他のジョブの種類よりも使用するリソースが少なくなります。

重要

また、ジョブを作成した親セッションは、ジョブの状態を監視し、パイプライン データを収集します。 ジョブの子プロセスは、ジョブが完了した状態に達すると、親プロセスによって終了されます。 親セッションが終了すると、実行中のすべての子ジョブが子プロセスと共に終了します。

この状況を回避するには、次の 2 つの方法があります。

  1. 切断されたセッションで実行されるジョブを作成するために使用 Invoke-Command します。 詳細については、「about_Remote_Jobs」を参照してください
  2. ジョブではなく新しいプロセスを作成するために使用 Start-Process します。 詳細については、「Start-Process」を参照してください。

スレッド ベースのジョブを開始および管理する方法

スレッド ベースのジョブを開始するには、次の 2 つの方法があります。

  • Start-ThreadJob- ThreadJob モジュールから
  • ForEach-Object -Parallel -AsJob - PowerShell 7.0 で並列機能が追加されました

スレッド ベースのジョブを管理するには、about_Jobs説明されているのと同じジョブ コマンドレットを使用します。

Start-ThreadJob の使用

ThreadJob モジュールは、最初に PowerShell 6 に付属しています。 Windows PowerShell 5.1 のPowerShell ギャラリーからインストールすることもできます。

ローカル コンピューターでスレッド ジョブを開始するには、コマンドまたはスクリプトを Start-ThreadJob 中かっこ ({ }) で囲んだコマンドレットを使用します。

次の例では、ローカル コンピューターでコマンドを Get-Process 実行するスレッド ジョブを開始します。

Start-ThreadJob -ScriptBlock { Get-Process }

このコマンドは Start-ThreadJob 、実行中のジョブを ThreadJob 表すオブジェクトを返します。 ジョブ オブジェクトには、現在の実行中の状態を含む、ジョブに関する有用な情報が含まれています。 結果が生成されると、ジョブの結果が収集されます。

ForEach-Object -Parallel -AsJob の使用

PowerShell 7.0 では、コマンドレットに新しいパラメーター セットが ForEach-Object 追加されました。 新しいパラメーターを使用すると、PowerShell ジョブとして並列スレッドでスクリプト ブロックを実行できます。

にデータを ForEach-Object -Parallelパイプできます。 データは、並列で実行されるスクリプト ブロックに渡されます。 このパラメーターは -AsJob 、並列スレッドごとにジョブ オブジェクトを作成します。

次のコマンドは、コマンドにパイプされた各入力値の子ジョブを含むジョブを開始します。 各子ジョブは、 Write-Output パイプされた入力値を引数として使用してコマンドを実行します。

1..5 | ForEach-Object -Parallel { Write-Output $_ } -AsJob

このコマンドは ForEach-Object -ParallelPSTaskJob パイプされた各入力値の子ジョブを含むオブジェクトを返します。 ジョブ オブジェクトには、状態を実行している子ジョブに関する有用な情報が含まれています。 結果が生成されると、子ジョブの結果が収集されます。

ジョブの完了を待機し、ジョブの結果を取得する方法

PowerShell ジョブ コマンドレットを使用して、Wait-JobReceive-Jobジョブが完了するのを待ってから、ジョブによって生成されたすべての結果を返すことができます。

次のコマンドは、コマンドを実行するスレッド ジョブを Get-Process 開始し、コマンドが完了するまで待機し、最後にコマンドによって生成されたすべてのデータ結果を返します。

Start-ThreadJob -ScriptBlock { Get-Process } | Wait-Job | Receive-Job

次のコマンドは、パイプされた各入力に対してコマンドを Write-Output 実行するジョブを開始し、その後、すべての子ジョブが完了するのを待機し、最後に子ジョブによって生成されたすべてのデータ結果を返します。

1..5 | ForEach-Object -Parallel { Write-Output $_ } -AsJob | Wait-Job | Receive-Job

コマンドレットは Receive-Job 、子ジョブの結果を返します。

1
3
2
4
5

各子ジョブは並列実行されるため、生成された結果の順序は保証されません。

スレッド ジョブのパフォーマンス

スレッド ジョブは、他の種類のジョブよりも高速で軽量です。 ただし、ジョブが実行している作業と比較すると、オーバーヘッドが大きくなる可能性があります。

PowerShell は、セッションでコマンドとスクリプトを実行します。 セッションで一度に実行できるコマンドまたはスクリプトは 1 つだけです。 そのため、複数のジョブを実行する場合、各ジョブは個別のセッションで実行されます。 各セッションはオーバーヘッドに寄与します。

スレッド ジョブは、実行する作業がジョブの実行に使用されるセッションのオーバーヘッドよりも大きい場合に最適なパフォーマンスを提供します。 この条件を満たすケースは 2 つあります。

  • 処理はコンピューティング集中型です。複数のスレッド ジョブでスクリプトを実行すると、複数のプロセッサ コアを利用して、より高速に完了できます。

  • 作業は重要な待機で構成されます。I/O またはリモート呼び出しの結果を待機する時間を費やすスクリプトです。 通常、並列実行は、順番に実行する場合よりも速く完了します。

(Measure-Command {
    1..1000 | ForEach { Start-ThreadJob { Write-Output "Hello $using:_" } } | Receive-Job -Wait
}).TotalMilliseconds
36860.8226

(Measure-Command {
    1..1000 | ForEach-Object { "Hello: $_" }
}).TotalMilliseconds
7.1975

上の最初の例は、単純な文字列書き込みを行うために 1000 個のスレッド ジョブを作成する foreach ループを示しています。 ジョブのオーバーヘッドにより、完了までに 36 秒以上かかります。

2 番目の例では、 ForEach 同じ 1000 操作を実行するコマンドレットを実行します。 今回は、 ForEach-Object ジョブのオーバーヘッドなしで、1 つのスレッドで順番に実行されます。 わずか 7 ミリ秒で完了します。

次の例では、10 個の個別のシステム ログに対して最大 5,000 個のエントリが収集されます。 スクリプトには多数のログの読み取りが含まれるため、操作を並列で行うのが理にかなっています。

$logNames.count
10

Measure-Command {
    $logs = $logNames | ForEach-Object {
        Get-WinEvent -LogName $_ -MaxEvents 5000 2>$null
    }
}

TotalMilliseconds : 252398.4321 (4 minutes 12 seconds)
$logs.Count
50000

このスクリプトは、ジョブが並列で実行されるときに半分の時間で完了します。

Measure-Command {
    $logs = $logNames | ForEach {
        Start-ThreadJob {
            Get-WinEvent -LogName $using:_ -MaxEvents 5000 2>$null
        } -ThrottleLimit 10
    } | Wait-Job | Receive-Job
}

TotalMilliseconds : 115994.3 (1 minute 56 seconds)
$logs.Count
50000

スレッド ジョブと変数

スレッド ベースのジョブに値を渡す方法は複数あります。

Start-ThreadJobは、コマンドレットにパイプ処理される変数、キーワード (keyword)を介して$usingスクリプト ブロックに渡される変数、または ArgumentList パラメーターを使用して渡される変数を受け取ることができます。

$msg = "Hello"

$msg | Start-ThreadJob { $input | Write-Output } | Wait-Job | Receive-Job

Start-ThreadJob { Write-Output $using:msg } | Wait-Job | Receive-Job

Start-ThreadJob { param ([string] $message) Write-Output $message } -ArgumentList @($msg) |
  Wait-Job | Receive-Job

ForEach-Object -Parallelは、パイプされた変数を受け取り、キーワード (keyword)を介してスクリプト ブロックに直接渡される変数を$using受け取ります。

$msg = "Hello"

$msg | ForEach-Object -Parallel { Write-Output $_ } -AsJob | Wait-Job | Receive-Job

1..1 | ForEach-Object -Parallel { Write-Output $using:msg } -AsJob | Wait-Job | Receive-Job

スレッド ジョブは同じプロセスで実行されるため、ジョブに渡される変数参照型は慎重に処理する必要があります。 スレッド セーフ オブジェクトでない場合は、割り当てないようにし、メソッドとプロパティを呼び出すべきではありません。

次の例では、スレッド セーフな .NET ConcurrentDictionary オブジェクトをすべての子ジョブに渡して、一意の名前のプロセス オブジェクトを収集します。 スレッド セーフ オブジェクトであるため、ジョブがプロセス内で同時に実行されている間は安全に使用できます。

$threadSafeDictionary = [System.Collections.Concurrent.ConcurrentDictionary[string,object]]::new()
$jobs = Get-Process | ForEach {
    Start-ThreadJob {
        $proc = $using:_
        $dict = $using:threadSafeDictionary
        $dict.TryAdd($proc.ProcessName, $proc)
    }
}
$jobs | Wait-Job | Receive-Job

$threadSafeDictionary.Count
96

$threadSafeDictionary["pwsh"]

NPM(K)  PM(M)   WS(M) CPU(s)    Id SI ProcessName
------  -----   ----- ------    -- -- -----------
  112  108.25  124.43  69.75 16272  1 pwsh

関連項目