ちょっと前の話なのですが、PowerShell Core 6.1 Preview.4からジョブ関連でThreadJobという新機能が追加されました。
従来のジョブ
これまでのPowerShellではStart-Job
などのコマンドを使うことで重たい処理をバックグラウンドで実行させるジョブの仕組みが提供されてきました。
PowerShell Core 6.0からはStart-Job
の代わりに&
を使うこともできます。
このジョブの仕組みは、個々のジョブをPowerShellのプロセスとして実行するものであるため、ジョブの独立性には優れているものの、複数のジョブを同時に並列して実行する様な場合はプロセス起動のコストの高さからパフォーマンスが急激に悪くなってしまう問題を抱えていました。
(PowerShellのジョブ実行の仕組み)
例として以下の様な単純なジョブでも10並列にすると異常に時間がかかってしまいます。
Measure-Command { # 10並列でジョブ実行 1..10 | ForEach-Object { # 与えられた引数を表示するだけの単純なジョブを実行 Start-Job -ArgumentList $_ -ScriptBlock { param($arg) Write-Output "arg = $arg" } } # ジョブの終了待ち Get-Job | Receive-Job -Wait -OutVariable Output } # 個々のジョブの処理時間を表示 Get-Job | Select-Object Id, Name, PSBeginTime, PSEndTime
手元の環境(Surface Pro4)で約22秒もかかっています。
ちなみにこの処理を実行しているときはこんな感じで大量のPowerShellプロセスが起動されリソースを消費していることが分かります。
ThreadJob
この問題を解消するために導入されたのがThreadJobになります。
こちらは中の人であるPaulさんのモジュールとして機能が実装され、PowerShell Core 6.1 Preview.4より標準に組み込まれる形になっています。
このモジュールで提供される機能はStart-ThreadJob
コマンドただ一つです。
その使い方は基本的にはStart-Job
と同じで、Start-ThreadJob
コマンドでジョブが作られ、作られたジョブはGet-Job
、Receive-Job
、Remove-Job
といったコマンドで扱うことができます。
従来のジョブと違うのはPSJobTypeName
の内容がThreadJob
となっているくらいでしょうか。
ThreadJobの仕組み
ThreadJobでは個々のジョブは別プロセスとはならず、同一プロセス内でRunspaceを分けることで行われています。
(ThreadJobの仕組み)
このRunspaceを分けることで並列処理を行う手法自体は昔からあるもので、詳細については以下のブログを見ると参考になるでしょう。
ThreadJobはこの並列処理の手法をPowerShellのジョブのインターフェイスに合わせたものとなります。
性能差
ThreadJobは同一プロセス内の別Runspaceで実行されるため従来のジョブほどの独立性はありませんが、起動コストが少なく複数のジョブを並列実行するのに向いています。
最初に示したジョブの例をThreadJobで試してみると、約0.3秒と1秒もかからずに終わってしまいます。
Measure-Command { # 10並列でThreadJob実行 1..10 | ForEach-Object { # 与えられた引数を表示するだけの単純なジョブを実行 Start-ThreadJob -ArgumentList $_ -ScriptBlock { param($arg) Write-Output "arg = $arg" } } # ジョブの終了待ち Get-Job | Receive-Job -Wait -OutVariable Output } # 個々のジョブの処理時間を表示 Get-Job | Select-Object Id, Name, PSBeginTime, PSEndTime
Windows PowerShellでのThreadJobの利用
このTreadJobはモジュールとして提供されているため、PowerShell CoreだけでなくWindows PowerShell(PowerShell 3.0以降)でも利用可能です。
インストール方法はいろいろありますがInstall-Module
をするのが手っ取り早いでしょう。
Install-Module ThreadJob -Scope CurrentUser
先ほどの例もこの様にサクッと利用可能です。
従来のジョブとThreadJobの使い分け
最後に従来のジョブとThreadJobの使い分けについて簡単に私見を述べたいと思います。
本エントリの例で示した様に単純な処理時間だけをみるとThreadJobだけ使えば良さそうに見えます。
性能は重要な要素ですのでThreadJobが使える環境では積極的にThreadJobを使うのが良いと思います。
ただし、従来のジョブにもメリットはあります。
個々のジョブが別プロセスであるため、仮にジョブ内部で予期しない例外が発生した場合でも呼び出し元に影響を及ぼすことは決してありません。
また、例えばメモリなどのリソースを大量に消費する処理をジョブにした場合、呼び出し元にそのリソース消費が影響することはありませんし、ジョブが終了されればプロセス自体が消えてしまうためリソースの解放漏れといったことを気にする必要が一切ありません。
ThreadJobではRunSpaceが分かれていても同一プロセスであるため、例外の種類によってはプロセス自体が落ちてしまう可能性は残りますし、リソースの消費や解放漏れについても気にかけてやる必要があります。
実行するジョブの内容に応じて従来のジョブのメリットを得ることができるのであればそれを利用するのが良いでしょう。
もしくは単純に
重たい処理には従来のジョブ、大量の並列処理にはThreadJob。
くらいの使い分けでも良いかと思います。