PowerShell Job を使ってみる
TL;DR
- PowerShell で並列実行し隊
- Workflow と Job、ThreadJob という選択肢があるが、ThreadJob がよさそう
- 実行数を制御するときは
-ThrottleLimit
を指定するとよい
はじめに
なんか PowerShell Workflow を調べている中で、Job ってのもあるので使ってみます。
ざっくりいうとこちらも並列実行ができるようですが、いろいろと違いがあります。
こちらの記事は Azure VM として構築した Windows 10 の上で検証しています。
PowerShell 5.1.x 系が使いたかったということで、一時的にこちらの環境を試しています。
> $PSVersionTable
Name Value
---- -----
PSVersion 5.1.19041.2673
PSEdition Desktop
PSCompatibleVersions {1.0, 2.0, 3.0, 4.0...}
BuildVersion 10.0.19041.2673
CLRVersion 4.0.30319.42000
WSManStackVersion 3.0
PSRemotingProtocolVersion 2.3
SerializationVersion 1.1.0.1
ThreadJob
module を使ってみる
普通の PowerShell Job、まぁまぁ遅いんですよね。
複数の Job を走らせつつ Task Manager を見てみると、powershell.exe が複数起動しており、multi process で動いているという感じなんですね。
これに対して、multi thread の方が速いだろうということで利用できるのが ThreadJob
module です。
使い方は特に難しくはなくて、Start-Job
を Start-ThreadJob
に置き換えるだけです。
事前作業として、Install-Module -Name ThreadJob -Scope CurrentUser
でインストールしておきます。
というわけで、ほぼ差がないように見えますが TheadJob
版がこちらです。
$Svcs = Get-Service | Select-Object -First 3
$ScriptBlock = {
param(
[Parameter(Mandatory = $true)] $Svc
)
Write-Output "$(Get-Date -Format "o") The status of $($Svc.Name) is $(($Svc.Status).ToString())."
}
# Start Job for each VM
$Jobs = foreach ($Svc in $Svcs) {
Start-ThreadJob -ScriptBlock $ScriptBlock -ArgumentList $Svc
}
# Wait for all Jobs to complete
$Results = $Jobs | Wait-Job | Receive-Job
$Results
一例でしかないのですが、1 行目を 30 に変えて 30 つの Windows service について情報を出力させると、検証マシンでは以下のような差が出ました。
Start-Job
は 84.36 sec、Start-ThreadJob
では 1.55 sec でした。
特に Start-Job
の方は速くなったり遅くなったりの違いがかなり大きいです。。
ThreadJob
は -ThrottleLimit
で並列実行数を変えられる
Start-ThreadJob
に -ThrottleLimit
を指定すると、並列実行数を変えられます。
特に意味はないですが、Start-Sleep
を使った具体的な例を挙げてみます。
まずは ThreadJob
を使わずに Start-Job
を使った場合です。
結構かかってますね。。
> Measure-Command {1..30 | % {Start-Job {Start-Sleep 1}} | Wait-Job} | Select-Object TotalSeconds
TotalSeconds
------------
93.7860297
これに対し、-ThrottleLimit
を指定しない場合の Start-ThreadJob
では以下のとおりです。
> Measure-Command {1..30 | % {Start-ThreadJob {Start-Sleep 1}} | Wait-Job} | Select-Object TotalSeconds
TotalSeconds
------------
7.1880552
これに対して、-ThrottleLimit 30
を指定して、理論値最速の 30 並列で実行してみます。
> Measure-Command {1..30 | % {Start-ThreadJob -ThrottleLimit 30 {Start-Sleep 1}} | Wait-Job} | Select-Object TotalSeconds
TotalSeconds
------------
3.0249063
ということで確かに速くなることが確認できています。
この並列化が PowerShell 5.1.x でも動いているのはうれしいです。
Write-(Output|Warning|Verbose)
とかの出力系 cmdlet の使い方にちょっと癖がある
以降、気に入ったので ThreadJob
を使っていきます。
今までは Write-Output
を使っていたのですが、Write-Warning
とか Write-Verbose
を使う場面もあるかと思うのですが、少し癖があります。
以下の PowerShell script を実行してみます。
$Svcs = Get-Service | Select-Object -First 3
$ScriptBlock = {
param(
[Parameter(Mandatory = $true)] $Svc
)
Write-Output "$(Get-Date -Format "o") The status of $($Svc.Name) is $(($Svc.Status).ToString())."
Write-Warning "$(Get-Date -Format "o") The status of $($Svc.Name) is $(($Svc.Status).ToString())."
Write-Verbose "$(Get-Date -Format "o") The status of $($Svc.Name) is $(($Svc.Status).ToString())."
}
# Start Job for each VM
$Jobs = foreach ($Svc in $Svcs) {
Start-ThreadJob -ScriptBlock $ScriptBlock -ArgumentList $Svc
}
# Wait for all Jobs to complete
$Results = $Jobs | Wait-Job | Receive-Job
$Results
実行結果は期待どおり以下のような感じです。
細かく見ていくと、Receive-Job
実行時点で Write-Warning
の出力が画面に表示され、Write-Output
の出力は $Results
に格納されているようです。
Write-Verbose
の出力はどこにも表示されていません。
> $Results = $Jobs | Wait-Job | Receive-Job
WARNING: 2023-05-04T12:42:43.9376178+00:00 The status of AarSvc_10deec is Stopped.
WARNING: 2023-05-04T12:42:43.9532433+00:00 The status of AdtAgent is Stopped.
WARNING: 2023-05-04T12:42:43.9844960+00:00 The status of AJRouter is Stopped.
> $Results
2023-05-04T12:42:43.9376178+00:00 The status of AarSvc_10deec is Stopped.
2023-05-04T12:42:43.9532433+00:00 The status of AdtAgent is Stopped.
2023-05-04T12:42:43.9844960+00:00 The status of AJRouter is Stopped.
Verbose な output を画面に表示させるためには、$VerbosePreference = 'Continue'
を実行しておく必要があります。
のはずなのですが、実はこれでも Write-Verbose
の内容は画面に表示されませんでした。
いくつか試したところ、$ScriptBlock
の中にも $VerbosePreference = 'Continue'
を書いておかないと出力されないようでした。
$Svcs = Get-Service | Select-Object -First 3
$ScriptBlock = {
param(
[Parameter(Mandatory = $true)] $Svc
)
$VerbosePreference = 'Continue'
Write-Output "$(Get-Date -Format "o") The status of $($Svc.Name) is $(($Svc.Status).ToString())."
Write-Warning "$(Get-Date -Format "o") The status of $($Svc.Name) is $(($Svc.Status).ToString())."
Write-Verbose "$(Get-Date -Format "o") The status of $($Svc.Name) is $(($Svc.Status).ToString())."
}
# Start Job for each VM
$Jobs = foreach ($Svc in $Svcs) {
Start-ThreadJob -ScriptBlock $ScriptBlock -ArgumentList $Svc
}
# Wait for all Jobs to complete
$Results = $Jobs | Wait-Job | Receive-Job
$Results
こちらの実行結果は以下のとおりです。
ということで、VERBOSE:
で始まる行で Write-Verbose
の出力が画面に表示されていることがわかります。
> $Results = $Jobs | Wait-Job | Receive-Job
VERBOSE: 2023-05-04T12:48:49.1369121+00:00 The status of AarSvc_10deec is Stopped.
WARNING: 2023-05-04T12:48:49.1369121+00:00 The status of AarSvc_10deec is Stopped.
VERBOSE: 2023-05-04T12:48:49.1525379+00:00 The status of AdtAgent is Stopped.
WARNING: 2023-05-04T12:48:49.1525379+00:00 The status of AdtAgent is Stopped.
VERBOSE: 2023-05-04T12:48:49.1837871+00:00 The status of AJRouter is Stopped.
WARNING: 2023-05-04T12:48:49.1837871+00:00 The status of AJRouter is Stopped.
> $Results
2023-05-04T12:48:49.1369121+00:00 The status of AarSvc_10deec is Stopped.
2023-05-04T12:48:49.1525379+00:00 The status of AdtAgent is Stopped.
2023-05-04T12:48:49.1837871+00:00 The status of AJRouter is Stopped.
あらかじめ $ScriptBlock
の中に $VerbosePreference = 'Continue'
を書いておけば、それを実行する PowerShell session の設定に応じて出力が変わったようにできるかとは思います。
$ScriptBlock
で渡す際に型情報が引き継がれている
以下のような PowerShell script を実行してみます。
$Svcs = Get-Service | Select-Object -First 3
$ScriptBlock = {
param(
[Parameter(Mandatory = $true)] $Svc
)
Write-Output "$(Get-Date -Format "o") The type of `$Svc is $($Svc.GetType().FullName)."
}
# Start Job for each VM
$Jobs = foreach ($Svc in $Svcs) {
Start-ThreadJob -ScriptBlock $ScriptBlock -ArgumentList $Svc
}
# Wait for all Jobs to complete
$Results = $Jobs | Wait-Job | Receive-Job
$Results
実行結果は以下のとおりです。
3 行表示する意味はないのですが、いずれにせよ System.ServiceProcess.ServiceController
という型情報が引き継がれていることがわかります。
これは別途記事を書いた PowerShell Workflow との大きな違いとなります。
> $Results
2023-05-04T12:51:54.9084622+00:00 The type of $Svc is System.ServiceProcess.ServiceController.
2023-05-04T12:51:54.9397135+00:00 The type of $Svc is System.ServiceProcess.ServiceController.
2023-05-04T12:51:54.9553375+00:00 The type of $Svc is System.ServiceProcess.ServiceController.
型情報が引き継がれていることで、深い property にも問題なくアクセスできるわけで、これは余計なことをしなくてよい、大きなメリットとなります。
まとめ
Install-Module
が必要ではありますが非管理者でも可能かとは思いますし、PowerShell 5.1.x 系におけるタスク並列化としては ThreadJob
一択でいいんじゃないかという印象です。
もう少し大き目のタスクについてもこれから流し込んで試してみます。
参考
- ジョブについて - PowerShell
- スクリプト ブロックについて - PowerShell
- スレッド ジョブについて - PowerShell
- 出力ストリームについて - PowerShell
Write-Output
とかWrite-Verbose
についてはこちらに一覧があります
- 基本設定変数について - PowerShell
$VerbosePreference = 'Continue'
とか、それ以外にもいろいろあるのですが、こちらに一覧があります
- PowerShell Workflow についてはこちらに記事を書きました
Update log
- 全体的に
# (見出し1)
から## (見出し2)
に変更 - 2024/01/24 - ThreadJob のほうがイケてるので順番を入れ替えました - 2024/01/24
PowerShell Job の基本的な使い方
まずは簡単なものを試してみます。
$Svcs = Get-Service | Select-Object -First 3
# Start Job for each VM
$Jobs = foreach ($Svc in $Svcs) {
Start-Job -ScriptBlock {
Write-Output "$(Get-Date -Format "o") The status of $($Using:Svc.Name) is $(($Using:Svc.Status).ToString())."
}
}
# Wait for all Jobs to complete
$Results = $Jobs | Wait-Job | Receive-Job
$Results
少し時間がかかるかもしれませんが、$Result
に以下のような結果が入っているはずです。
細かいサービスの内容についてはここでは関係ないので触れません。
> $Results
2023-05-04T11:56:35.6122188+00:00 The status of AarSvc_10deec is Stopped.
2023-05-04T11:56:35.6747190+00:00 The status of AdtAgent is Stopped.
2023-05-04T11:56:35.5809681+00:00 The status of AJRouter is Stopped.
んで、$ScriptBlock
という変数を定義することで、以下のように書き換えることができます。
$Svcs = Get-Service | Select-Object -First 3
$ScriptBlock = {
Write-Output "$(Get-Date -Format "o") The status of $($Using:Svc.Name) is $(($Using:Svc.Status).ToString())."
}
# Start Job for each VM
$Jobs = foreach ($Svc in $Svcs) {
Start-Job -ScriptBlock $ScriptBlock
}
# Wait for all Jobs to complete
$Results = $Jobs | Wait-Job | Receive-Job
$Results
また、$Using
というのは少し慣れないな、という場合には以下のようにも書くことができます。
こちらのほうが、$ScriptBlock
の部分を関数のように書くことができ、[Parameter(Mandatory = $true)]
などと引数に対する属性指定などもできるため便利かと思います。
$Svcs = Get-Service | Select-Object -First 3
$ScriptBlock = {
param(
[Parameter(Mandatory = $true)] $Svc
)
Write-Output "$(Get-Date -Format "o") The status of $($Svc.Name) is $(($Svc.Status).ToString())."
}
# Start Job for each VM
$Jobs = foreach ($Svc in $Svcs) {
Start-Job -ScriptBlock $ScriptBlock -ArgumentList $Svc
}
# Wait for all Jobs to complete
$Results = $Jobs | Wait-Job | Receive-Job
$Results
`Start-Job` のよくわからん動作
これ、よくわからないんですけど、Get-NetAdapter
で取得したオブジェクトを使うと動きません。。
Wait-Job
の部分で止まってしまい、結果が返ってきません。。
$Nics = Get-NetAdapter
$ScriptBlock = {
param(
[Parameter(Mandatory = $true)] $Nic
)
Write-Output "$(Get-Date -Format "o") The status of $($Nic.Name) is $(($Nic.Status).ToString())."
}
# Start Job for each VM
$Jobs = foreach ($Nic in $Nics) {
Start-Job -ScriptBlock $ScriptBlock -ArgumentList $Nic
}
# Wait for all Jobs to complete
$Results = $Jobs | Wait-Job | Receive-Job
$Results
原因はわかっていないですが、-ScriptBlock
の部分を直書きすると動きます。。
$Nics = Get-NetAdapter
# Start Job for each VM
$Jobs = foreach ($Nic in $Nics) {
Start-Job -ScriptBlock {
Write-Output "$(Get-Date -Format "o") The status of $($Using:Nic.Name) is $(($Using:Nic.Status).ToString())."
}
}
# Wait for all Jobs to complete
$Results = $Jobs | Wait-Job | Receive-Job
$Results
明らかではありますが、実行結果は以下のような感じです。
> $Results
2023-05-04T12:07:10.8600213+00:00 The status of Ethernet is Up.
よくわかってないのですが、後述の ThreadJob
が気に入ってしまったのでこちらは放置します。。
Discussion