しばたテックブログ

気分で書いている技術ブログです。

PowerShell Core 6.1で導入されるThreadJobについて

ちょっと前の話なのですが、PowerShell Core 6.1 Preview.4からジョブ関連でThreadJobという新機能が追加されました。

従来のジョブ

これまでのPowerShellではStart-Jobなどのコマンドを使うことで重たい処理をバックグラウンドで実行させるジョブの仕組みが提供されてきました。
PowerShell Core 6.0からはStart-Jobの代わりに&を使うこともできます。

blog.shibata.tech

このジョブの仕組みは、個々のジョブをPowerShellのプロセスとして実行するものであるため、ジョブの独立性には優れているものの、複数のジョブを同時に並列して実行する様な場合はプロセス起動のコストの高さからパフォーマンスが急激に悪くなってしまう問題を抱えていました。

f:id:stknohg:20180905174141p:plain

(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秒もかかっています。

f:id:stknohg:20180905174308p:plain

ちなみにこの処理を実行しているときはこんな感じで大量のPowerShellプロセスが起動されリソースを消費していることが分かります。

f:id:stknohg:20180905174415p:plain

ThreadJob

この問題を解消するために導入されたのがThreadJobになります。

こちらは中の人であるPaulさんのモジュールとして機能が実装され、PowerShell Core 6.1 Preview.4より標準に組み込まれる形になっています。

github.com

このモジュールで提供される機能はStart-ThreadJobコマンドただ一つです。
その使い方は基本的にはStart-Jobと同じで、Start-ThreadJobコマンドでジョブが作られ、作られたジョブはGet-JobReceive-JobRemove-Jobといったコマンドで扱うことができます。

f:id:stknohg:20180905174450p:plain

従来のジョブと違うのはPSJobTypeNameの内容がThreadJobとなっているくらいでしょうか。

ThreadJobの仕組み

ThreadJobでは個々のジョブは別プロセスとはならず、同一プロセス内でRunspaceを分けることで行われています。

f:id:stknohg:20180905174508p:plain

(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

f:id:stknohg:20180905174617p:plain

Windows PowerShellでのThreadJobの利用

このTreadJobはモジュールとして提供されているため、PowerShell CoreだけでなくWindows PowerShell(PowerShell 3.0以降)でも利用可能です。

インストール方法はいろいろありますがInstall-Moduleをするのが手っ取り早いでしょう。

Install-Module ThreadJob -Scope CurrentUser

先ほどの例もこの様にサクッと利用可能です。

f:id:stknohg:20180905174650p:plain

従来のジョブとThreadJobの使い分け

最後に従来のジョブとThreadJobの使い分けについて簡単に私見を述べたいと思います。

本エントリの例で示した様に単純な処理時間だけをみるとThreadJobだけ使えば良さそうに見えます。
性能は重要な要素ですのでThreadJobが使える環境では積極的にThreadJobを使うのが良いと思います。

ただし、従来のジョブにもメリットはあります。
個々のジョブが別プロセスであるため、仮にジョブ内部で予期しない例外が発生した場合でも呼び出し元に影響を及ぼすことは決してありません。
また、例えばメモリなどのリソースを大量に消費する処理をジョブにした場合、呼び出し元にそのリソース消費が影響することはありませんし、ジョブが終了されればプロセス自体が消えてしまうためリソースの解放漏れといったことを気にする必要が一切ありません。
ThreadJobではRunSpaceが分かれていても同一プロセスであるため、例外の種類によってはプロセス自体が落ちてしまう可能性は残りますし、リソースの消費や解放漏れについても気にかけてやる必要があります。

実行するジョブの内容に応じて従来のジョブのメリットを得ることができるのであればそれを利用するのが良いでしょう。

もしくは単純に

重たい処理には従来のジョブ、大量の並列処理にはThreadJob。

くらいの使い分けでも良いかと思います。