PowerShellマルチスレッディング:詳細な解説

ほとんどの人は、基本的なPowerShellスクリプトでは問題を解決するのに時間がかかりすぎるという問題に直面することがあります。ネットワーク上の多くのコンピュータからデータを収集したり、一度にたくさんの新しいユーザーをActive Directoryに作成する場合などが該当します。これらは、より多くの処理能力を使用することでコードの実行を高速化できる素晴らしい例です。では、PowerShellのマルチスレッドを使用してこれを解決する方法について見ていきましょう!

デフォルトのPowerShellセッションはシングルスレッドです。1つのコマンドを実行してから次のコマンドに移動します。これは、すべてを繰り返し可能に保ち、多くのリソースを使用しないため、便利です。しかし、実行中のアクションが互いに依存しない場合や、余分なCPUリソースがある場合はどうでしょうか?その場合は、マルチスレッドについて考える時が来たのです。

この記事では、PowerShellのマルチスレッド技術を理解し、複数のデータストリームを同時に処理するためのさまざまな方法を学びますが、すべて同じコンソールで管理します。

PowerShellのマルチスレッドの理解

マルチスレッドは、複数のコマンドを同時に実行する方法です。通常、PowerShellは単一のスレッドを使用しますが、複数のスレッドを使用してコードを並列化する方法はさまざまあります。

マルチスレッドの主な利点は、コードの実行時間を短縮することです。ただし、この時間の短縮には、より高い処理能力が必要です。マルチスレッドを使用すると、複数のアクションが同時に実行されるため、システムリソースがより多く必要とされます。

たとえば、Active Directoryで新しいユーザーを1人作成したい場合はどうなりますか?この例では、実行されるコマンドは1つだけなので、マルチスレッドは必要ありません。しかし、1000人の新しいユーザーを作成したい場合は、状況は変わります。

マルチスレッドを使用しない場合、1000回のNew-ADUserコマンドを実行して、すべてのユーザーを作成する必要があります。新しいユーザーを作成するのに3秒かかるとします。1000人のユーザーを作成するには、約1時間かかります。1つのスレッドで1000のコマンドを使用する代わりに、10個のコマンドを実行する100個のスレッドを使用することができます。これにより、50分かかる代わりに、1分未満で完了します!

完全なスケーリングは見られないことに注意してください。コード内でアイテムを起動・終了する作業には時間がかかります。単一のスレッドを使用する場合、PowerShellはコードを実行して完了します。複数のスレッドを使用する場合、元のスレッドは他のスレッドを管理するために使用されます。ある時点で、元のスレッドは他のスレッドを管理するだけで精一杯になるでしょう。

PowerShellのマルチスレッディングの前提条件

この記事では、実際に手を動かしながらPowerShellのマルチスレッディングの仕組みを学びます。一緒に進める場合、以下に必要なものと使用される環境の詳細があります。

  • Windows PowerShellのバージョンが3以上であること – 明示的に記載されていない限り、すべてのコードはWindows PowerShellのバージョン3以上で動作します。例ではWindows PowerShellのバージョン5.1を使用します。
  • 余分なCPUとメモリが必要です – PowerShellで並列処理を行うためには、少なくとも少し余分なCPUとメモリが必要です。これが利用できない場合、パフォーマンスの利点は得られないかもしれません。

優先順位1:コード修正!

PowerShellのマルチスレッドを使ってスクリプトの処理を高速化する前に、いくつかの準備作業を完了する必要があります。最初に、コードの最適化です。

コードの実行を高速化するために、リソースを追加することはできますが、マルチスレッドは追加の複雑さももたらします。マルチスレッドの前にコードを高速化する方法がある場合は、まずそれを行うべきです。

ボトルネックの特定

コードを並列化する最初のステップの1つは、処理を遅くしている要素を見つけることです。コードが遅い理由は、不正なロジックや余分なループなどである場合、マルチスレッドの前にいくつかの修正を加えることで高速化できるかもしれません。

コードを高速化する一般的な方法の例として、フィルタリングを左にシフトすることがあります。大量のデータとやり取りする場合、データの量を減らすためのフィルタリングはできるだけ早い段階で行うべきです。以下は、svchostプロセスの使用CPU量を取得するコードの例です。

以下の例は、すべての実行中のプロセスを読み取り、単一のプロセス(svchost)をフィルタリングします。その後、CPUプロパティを選択し、その値がnullでないことを確認します。

PS51> Get-Process | Where-Object {$_.ProcessName -eq 'svchost'} | 
	Select-Object CPU | Where-Object {$_.CPU -ne $null}

上記のコードを以下の例と比較してください。下記は同じ出力を持つ別のコードの例ですが、配置が異なります。下記のコードは、可能な限り左側のパイプ記号の前にすべてのロジックを移動することで、シンプルになっていることに注意してください。これにより、Get-Processが気にしないプロセスを返さないようになります。

PS51> Get-Process -Name 'svchost' | Where-Object {$_.CPU -ne $null} | 
	Select-Object CPU

上記の2つの行を実行するための時間差は以下の通りです。この117msの差は、このコードを1回だけ実行する場合は気づかれないかもしれませんが、何千回も実行すると差が積み重なることになります。

time difference for running the two lines from above

スレッドセーフなコードの使用

次に、コードが「スレッドセーフ」であることを確認してください。 「スレッドセーフ」という用語は、1つのスレッドがコードを実行しているときに、別のスレッドが同じコードを同時に実行しても競合が発生しないことを指します。

たとえば、2つの異なるスレッドで同じファイルに書き込む場合、それはスレッドセーフではありません。ファイルに変更が加えられない限り、ファイルから読み取る2つのスレッドはスレッドセーフです。両方のスレッドは同じ出力を取得します。

スレッドセーフでないPowerShellのマルチスレッドコードの問題は、一貫性のない結果が得られる可能性があることです。スレッドが競合を引き起こさないようにするために、正常に動作することもありますが、不具合のトラブルシューティングが困難になる場合もあります。

2つまたは3つのジョブしか実行していない場合、それらがすべて異なるタイミングでファイルに書き込む可能性があります。しかし、コードを20から30個のジョブにスケーリングすると、少なくとも2つのジョブが同時に書き込もうとする可能性は非常に低くなります。

PSJobsを使用した並列実行

スクリプトをマルチスレッド化する最も簡単な方法の1つは、PSJobsを使用することです。PSJobsには、Microsoft.PowerShell.Coreモジュールに組み込まれたコマンドレットがあります。Microsoft.PowerShell.CoreモジュールはPowerShellのバージョン3以降のすべてのバージョンに含まれています。このモジュールのコマンドを使用すると、バックグラウンドでコードを実行しながら、フォアグラウンドで異なるコードを実行できます。以下に、利用可能なすべてのコマンドを示します。

PS51> Get-Command *-Job
Get-Command *-Job output

ジョブの管理

すべてのPSJobは、11つの状態のいずれかに存在します。これらの状態は、PowerShellがジョブを管理する方法です。

以下に、ジョブが存在する可能性のある最も一般的な状態のリストがあります。

  • Completed – ジョブが終了し、出力データを取得するか、ジョブを削除できます。
  • Running – ジョブが現在実行中であり、ジョブを強制停止せずには削除できません。出力もまだ取得できません。
  • Blocked – ジョブはまだ実行中ですが、ホストは処理を続行する前に情報を入力するように要求されています。
  • 失敗しました – ジョブの実行中に終了エラーが発生しました。

ジョブの状態を取得するには、Get-Job コマンドを使用します。このコマンドはジョブのすべての属性を取得します。

以下は、状態が完了であるジョブの出力です。以下の例では、Start-Job コマンドを使用して、ジョブ内でStart-Sleep 5 のコードを実行しています。そのジョブの状態は、Get-Job コマンドを使用して返されます。

PS51> Start-Job -Scriptblock {Start-Sleep 5}
PS51> Get-Job
Get-Job output

ジョブの状態が完了になると、スクリプトブロック内のコードが実行され終了します。また、HasMoreData プロパティがFalseであることもわかります。これは、ジョブが終了した後に出力するものがなかったことを意味します。

以下は、ジョブの状態を説明するために使用される他の状態の例です。コマンド列から、abc秒スリープしようとしたためにジョブが失敗したことがわかります。

All jobs

新しいジョブの作成

前述のように、Start-Job コマンドを使用すると、ジョブでコードの実行を開始する新しいジョブを作成できます。ジョブを作成する際には、ジョブに使用するスクリプトブロックを指定します。PSJobは一意のID番号を持つジョブを作成し、ジョブの実行を開始します。

ここでの主な利点は、Start-Jobコマンドを実行する時間が、使用しているスクリプトブロックを実行する時間よりも短いということです。以下の画像で見るように、コマンドが完了するのに5秒かかる代わりに、ジョブを開始するのにわずか0.15秒しかかかりませんでした。

How long it takes to create a job

同じコードを実行するのにかかる時間の一部であるため、バックグラウンドでPSJobとして実行されたため、わずかな時間で同じコードを実行できました。コードを前面で実行して実際に5秒間スリープするのではなく、0.15秒でコードの設定と実行を開始しました。

ジョブの出力の取得

ジョブ内のコードが出力を返す場合があります。そのコードの出力を取得するには、Receive-Jobコマンドを使用します。 Receive-Jobコマンドは、PSJobを入力として受け取り、ジョブの出力をコンソールに書き込みます。ジョブが実行されている間に出力されたものはすべて保存されており、ジョブが取得されたときにその時点で保存されていたすべてを出力します。

以下のコードを実行する例です。これにより、Hello Worldという出力を書き込むジョブが作成され、開始されます。そして、ジョブからの出力を取得し、それをコンソールに出力します。

$Job = Start-Job -ScriptBlock {Write-Output 'Hello World'}
Receive-Job $Job
Receive-Job output

スケジュールされたジョブの作成

PSJobsとのやり取り方法のもう一つの方法は、スケジュールされたジョブを通じて行うことができます。スケジュールされたジョブは、タスクスケジューラで設定できるWindowsのスケジュールされたタスクに似ています。スケジュールされたジョブを使用すると、トリガーに基づいてバックグラウンドでPSJobを実行することができます。

ジョブトリガー

ジョブトリガーは、特定の時間、ユーザーログオン時、システム起動時など、さまざまなものになります。トリガーを一定の間隔で繰り返すこともできます。これらのトリガーは、New-JobTriggerコマンドで定義されます。このコマンドは、スケジュールされたジョブを実行するトリガーを指定するために使用されます。トリガーのないスケジュールされたジョブは手動で実行する必要がありますが、各ジョブには複数のトリガーを持つことができます。

トリガーがあるだけでなく、通常のPSJobで使用されるスクリプトブロックも持っていることになります。トリガーとスクリプトブロックの両方がある場合、次のセクションに示すように、Register-ScheduledJobコマンドを使用してジョブを作成します。このコマンドは、スケジュールされたジョブの属性(実行されるスクリプトブロックや、New-JobTriggerコマンドで作成されたトリガーなど)を指定するために使用されます。

デモ

おそらく、誰かがコンピュータにログインするたびに実行する必要があるPowerShellコードがあるかもしれません。これに対してスケジュールされたジョブを作成することができます。

これを行うには、まずNew-JobTriggerを使用してトリガーを定義し、以下に示すようにスケジュールされたジョブを定義します。このスケジュールされたジョブは、ログインするたびにログファイルに行を書き込みます。

$Trigger = New-JobTrigger -AtLogon
$Script = {"User $env:USERNAME logged in at $(Get-Date -Format 'y-M-d H:mm:ss')" | Out-File -FilePath C:\Temp\Login.log -Append}

Register-ScheduledJob -Name Log_Login -ScriptBlock $Script -Trigger $Trigger

上記のコマンドを実行すると、以下のように新しいジョブを作成するときと同様の出力が表示されます。ジョブのID、スクリプトブロック、その他の属性が表示されます。

Registering a scheduled job

数回のログイン試行後、以下のスクリーンショットから試行がログに記録されていることがわかります。

Example login.log text file

AsJobパラメータを利用する

ジョブを使用する別の方法は、多くのPowerShellコマンドに組み込まれているAsJobパラメータを使用する方法です。さまざまなコマンドがあるため、以下に示すようにGet-Commandを使用してそれらをすべて見つけることができます。

PS51> Get-Command -ParameterName AsJob

最も一般的なコマンドの1つは、Invoke-Commandです。通常、このコマンドを実行すると、コマンドがすぐに実行されます。一部のコマンドはすぐに返され、続けて作業を続けることができますが、一部のコマンドはコマンドの実行が終了するまで待機します。

AsJobパラメータを使用すると、実行されたコマンドをコンソールで同期的に実行する代わりに、ジョブとして実行します。

ほとんどの場合、AsJobはローカルマシンで使用できますが、Invoke-Commandにはローカルマシンで実行するためのネイティブなオプションはありません。その回避策として、ComputerNameパラメータの値としてLocalhostを使用する方法があります。以下にその回避策の例を示します。

PS51> Invoke-Command -ScriptBlock {Start-Sleep 5} -ComputerName localhost

AsJobパラメータを示すために、以下の例ではInvoke-Commandを使用して5秒間スリープし、その後同じコマンドをAsJobを使用して繰り返し実行して、実行時間の違いを示しています。

PS51> Measure-Command {Invoke-Command -ScriptBlock {Start-Sleep 5}}
PS51> Measure-Command {Invoke-Command -ScriptBlock {Start-Sleep 5} -AsJob -ComputerName localhost}
Comparing speed of jobs

ランスペース:ジョブよりも速い!

これまで、組み込みコマンドを使用してPowerShellで追加のスレッドを使用する方法について学んできました。スクリプトをマルチスレッド化する別のオプションは、別のランスペースを使用することです。

ランスペースは、PowerShellを実行するスレッドが操作する閉じられた領域です。PowerShellコンソールで使用されるランスペースは単一のスレッドに制限されていますが、追加のランスペースを使用して追加のスレッドを使用できます。

ランスペースとPSJobの比較

ランスペースとPSJobは多くの類似点を共有していますが、パフォーマンスには大きな違いがあります。ランスペースとPSJobの最大の違いは、それぞれのセットアップと解除にかかる時間です。

前のセクションの例では、作成されたPSJobの立ち上げに約150msかかりました。これは、ジョブのスクリプトブロックにはほとんどコードが含まれておらず、ジョブに追加の変数も渡されていないため、最良のケースです。

対照的に、ランスペースは事前に作成されます。ランスペースジョブの実行にかかるほとんどの時間は、コードが追加される前に処理されます。

以下は、PSJobに使用したのと同じコマンドをランスペースで実行する例です。

Measuring speed of job creation

対照的に、以下はrunspaceバージョンで使用されるコードです。同じタスクを実行するためには、はるかに多くのコードがあることがわかります。しかし、余分なコードの利点は、実行を開始するのに148msではなく36msでコマンドを実行できることです。

$Runspace = [runspacefactory]::CreateRunspace()
$PowerShell = [powershell]::Create()
$PowerShell.Runspace = $Runspace
$Runspace.Open()
$PowerShell.AddScript({Start-Sleep 5})
$PowerShell.BeginInvoke()
Measuring speed of creating a runspace

Runspacesの実行:ウォークスルー

最初はrunspacesを使うことは難しいかもしれません。なぜなら、もはやPowerShellのコマンドが手助けしてくれないからです。直接.NETクラスを扱わなければなりません。このセクションでは、PowerShellでrunspaceを作成するために必要な手順を解説します。

このウォークスルーでは、PowerShellコンソールから独立したrunspaceと別個のPowerShellインスタンスを作成します。その後、新しいrunspaceを新しいPowerShellインスタンスに割り当て、そのインスタンスにコードを追加します。

Runspaceの作成

まず、新しいrunspaceを作成する必要があります。これはrunspacefactoryクラスを使用して行います。以下に示すように、これを変数に格納して後で参照できるようにします。

 $Runspace = [runspacefactory]::CreateRunspace()

runspaceが作成されたので、PowerShellコードを実行するためにそれをPowerShellインスタンスに割り当てます。これにはpowershellクラスを使用し、runspaceと同様に、以下に示すようにこれを変数に格納する必要があります。

 $PowerShell = [powershell]::Create()

次に、runspaceをPowerShellインスタンスに追加し、コードを実行できるようにrunspaceを開き、スクリプトブロックを追加します。以下に、5秒間スリープするスクリプトブロックを示します。

 $PowerShell.Runspace = $Runspace
 $Runspace.Open()
 $PowerShell.AddScript({Start-Sleep 5})

Runspaceの実行

スクリプトブロックはまだ実行されていません。これまでに行われたのは、ランスペースのための全てを定義することです。スクリプトブロックの実行を開始するには、2つのオプションがあります。

  • Invoke()Invoke() メソッドはスクリプトブロックをランスペースで実行しますが、ランスペースが戻るまでコンソールには戻りません。これは、コードが正しく実行されるかをテストするために、コードを解放する前に確認するために役立ちます。
  • BeginInvoke()BeginInvoke() メソッドを使用すると、実際にパフォーマンスの向上が見られるでしょう。これにより、スクリプトブロックがランスペースで実行され、すぐにコンソールに戻ることができます。

BeginInvoke() を使用する場合は、スクリプトブロックのステータスを確認するために出力を変数に保存する必要があります。以下に示すように、これはランスペースのスクリプトブロックのステータスを確認するために必要です。

$Job = $PowerShell.BeginInvoke()

BeginInvoke() の出力を変数に保存したら、ジョブのステータスを確認するためにその変数をチェックできます。以下の例では、IsCompleted プロパティを使用しています。

the job is completed

もう一つの理由として、BeginInvoke() メソッドは Invoke() メソッドとは異なり、コードが完了した時に自動的に出力を返さないため、出力を変数に保存する必要があります。これを行うには、コードが完了した後に EndInvoke() メソッドを使用する必要があります。

この例では、出力はありませんが、Invoke を終了するためには以下のコマンドを使用します。

$PowerShell.EndInvoke($Job)

タスクがすべてキューに追加された後、常にランスペースを閉じる必要があります。これにより、PowerShellの自動ガベージコレクションプロセスが未使用のリソースをクリーンアップできます。以下は、これを行うために使用するコマンドです。

$Runspace.Close()

ランスペースプールの使用

ランスペースの使用はパフォーマンスを向上させますが、シングルスレッドの主な制限に直面します。これは、ランスペースプールが複数のスレッドを使用する点で優れているところです。

前のセクションでは、2つのランスペースのみを使用しました。PowerShellコンソール自体に1つ使用し、手動で作成したものに1つ使用しました。ランスペースプールを使用すると、1つの変数を使用してバックグラウンドで複数のランスペースを管理できます。

この複数ランスペースの動作は、複数のランスペースオブジェクトを使用して行うこともできますが、ランスペースプールを使用すると管理がはるかに簡単になります。

ランスペースプールは、シングルランスペースとは設定方法が異なります。主な違いの1つは、ランスペースプールで使用できるスレッドの最大数を定義することです。シングルランスペースでは1つのスレッドに制限されますが、プールではプールが拡張できる最大スレッド数を指定します。

ランスペースプールのスレッド数は、実行されるタスクの数とコードを実行しているマシンによって推奨されます。最大スレッド数を増やしても、ほとんどの場合には速度に影響が出ませんが、利点も得られない場合があります。

ランスペースプールの速度デモンストレーション

ランスペースプールが単一のランスペースよりも優れている例を示すために、10個の新しいファイルを作成したい場合を考えてみましょう。このタスクに単一のランスペースを使用する場合、最初のファイルを作成し、次に2番目のファイル、そして3番目のファイルと続けていき、すべての10個のファイルを作成します。以下の例では、このタスクに対するスクリプトブロックが定義されています。このスクリプトブロックには、ループ内で10個のファイル名を与え、それらがすべて作成されます。

$Scriptblock = {
    param($Name)
    New-Item -Name $Name -ItemType File
}

以下の例では、名前を受け取りその名前のファイルを作成する短いスクリプトが含まれるスクリプトブロックが定義されています。最大5つのスレッドを持つランスペースプールが作成されます。

次に、ループが10回繰り返され、各繰り返しでイテレーションの番号が$_に割り当てられます。したがって、最初の繰り返しでは1が、2番目の繰り返しでは2が代入されます。

ループはPowerShellオブジェクトを作成し、スクリプトブロックとスクリプトの引数を割り当て、プロセスを開始します。

最後に、ループの最後では、すべてのキュータスクが完了するまで待機します。

$Scriptblock = {
    param($Name)
    New-Item -Name $Name -ItemType File
}

$MaxThreads = 5
$RunspacePool = [runspacefactory]::CreateRunspacePool(1, $MaxThreads)
$RunspacePool.Open()
$Jobs = @()

1..10 | Foreach-Object {
	$PowerShell = [powershell]::Create()
	$PowerShell.RunspacePool = $RunspacePool
	$PowerShell.AddScript($ScriptBlock).AddArgument($_)
	$Jobs += $PowerShell.BeginInvoke()
}

while ($Jobs.IsCompleted -contains $false) {
	Start-Sleep 1
}

これにより、スレッドを1つずつ作成する代わりに、5つずつ作成することができます。ランスペースプールを使用しない場合、5つの別々のランスペースと5つの別々のPowershellインスタンスを作成して管理する必要があります。この管理はすぐに混乱します。

代わりに、ランスペースプールとPowerShellインスタンスを作成し、同じコードブロックと同じループを使用することができます。違いは、ランスペースがこれらの5つのスレッドを自動で使用することです。

ランスペースプールの作成

ランスペースプールの作成は、以前のセクションで作成したランスペースと非常に似ています。以下にその方法の例を示します。スクリプトブロックの追加と呼び出しプロセスは、ランスペースと同じです。以下のコードのように、ランスペースプールは最大5つのスレッドで作成されます。

$MaxThreads = 5
$RunspacePool = [runspacefactory]::CreateRunspacePool(1, $MaxThreads)
$PowerShell = [powershell]::Create()
$PowerShell.RunspacePool = $RunspacePool
$RunspacePool.Open()

速度のためのランスペースとランスペースプールの比較

ランスペースとランスペースプールの違いを示すために、以前に作成したランスペースでStart-Sleepコマンドを実行します。ただし、今回は10回実行する必要があります。以下のコードでは、5秒間スリープするランスペースが作成されています。

$Runspace = [runspacefactory]::CreateRunspace()
$PowerShell = [powershell]::Create()
$PowerShell.Runspace = $Runspace
$Runspace.Open()
$PowerShell.AddScript({Start-Sleep 5})

1..10 | Foreach-Object {
    $Job = $PowerShell.BeginInvoke()
    while ($Job.IsCompleted -eq $false) {Start-Sleep -Milliseconds 100}
}

1つのランスペースを使用しているため、別の呼び出しを開始する前に完了するまで待つ必要があります。これがジョブが完了するまで100msのスリープが追加されている理由です。これは削減できますが、ジョブが完了するのを待つよりもジョブが完了したかどうかを確認するのにより多くの時間を費やすため、収益は減少します。

以下の例からわかるように、10回の5秒間のスリープを完了するのに約51秒かかりました。

Measuring performance of creating runspaces

今度は単一のランスペースではなく、ランスペースプールを使用します。以下に実行されるコードが示されています。ランスペースプールを使用する場合、以下のコードで2つの使用方法の違いを見ることができます。

$RunspacePool = [runspacefactory]::CreateRunspacePool(1, 5)
$RunspacePool.Open()
$Jobs = @()

1..10 | Foreach-Object {
    $PowerShell = [powershell]::Create()
    $PowerShell.RunspacePool = $RunspacePool
    $PowerShell.AddScript({Start-Sleep 5})
    $Jobs += $PowerShell.BeginInvoke()
}
while ($Jobs.IsCompleted -contains $false) {Start-Sleep -Milliseconds 100}

以下の例では、たった10秒で完了するため、単一のランスペースの51秒に比べて大幅に改善されています。

Comparing runspaces vs. runspace pools

以下は、これらの例でランスペースとランスペースプールの違いをまとめた要約です。

Property Runspace Runspace Pool
Wait Delay Waiting for each job to finish before continuing to the next. Starting all of the jobs and then waiting until they have all finished.
Amount of Threads One Five
Runtime 50.8 Seconds 10.1 Seconds

PoshRSJobを使ってRunspacesに慣れる

A frequent occurrence when programming is that you will do what is more comfortable and accept the small loss in performance. This could be because it makes the code easier to write or easier to read, or it could just be your preference.

同じことがPowerShellでも起こりますが、使いやすさのために一部の人々はrunspaceの代わりにPSJobsを使用します。しかし、パフォーマンスを向上させるために難しくなりすぎないように、いくつかの方法があります。

一般的に使われているPoshRSJobというモジュールがあり、通常のPSJobsと同じスタイルのモジュールですが、runspaceを使用する利点があります。runspaceとPowerShellオブジェクトの作成に必要なすべてのコードを指定する必要がなく、PoshRSJobモジュールがコマンドを実行する際にそれらの処理をすべて行います。

モジュールをインストールするには、管理者権限のPowerShellセッションで以下のコマンドを実行します。

Install-Module PoshRSJob

モジュールがインストールされると、コマンドはPSJobコマンドと同じで、プレフィックスにRSが付きます。たとえば、Start-Jobの代わりにStart-RSJobGet-Jobの代わりにGet-RSJobとなります。

以下は、PSJobとRSJobで同じコマンドを実行する例です。構文と出力は非常に似ていますが、完全に同じではありません。

run the same command in a PSJob and then again in an RSJob

以下のコードは、PSJobとRSJobの速度の違いを比較するために使用できます。

Measure-Command {Start-Job -ScriptBlock {Start-Sleep 5}}
Measure-Command {Start-RSJob -ScriptBlock {Start-Sleep 5}}

下記の例では、RSJobsは依然としてrunspaceを使用しているため、速度の違いが大きくなっています。

large speed difference since the RSJobs are still using runspaces below the covers

Foreach-Object -Parallel

PowerShellコミュニティは、プロセスを簡単にマルチスレッド化するためのより簡単で組み込みの方法を求めていました。その結果、パラレルスイッチが生まれました。

この記事を書いている時点では、PowerShell 7はまだプレビュー版ですが、Foreach-ObjectコマンドにParallelパラメータが追加されています。このプロセスは、コードを並列化するためにランスペースを使用し、Foreach-Objectで使用されるスクリプトブロックをランスペースのスクリプトブロックとして使用します。

詳細はまだ詰められている最中ですが、これは将来的にランスペースを使用するより簡単な方法になるかもしれません。以下の例では、多くのスリープセットを素早くループすることができます。

Measure-Command {1..10 | Foreach-Object {Start-Sleep 5}}
Measure-Command {1..10 | Foreach-Object -Parallel {Start-Sleep 5}}
Looping through and measuring commands

マルチスレッディングの課題

マルチスレッディングはこれまでに素晴らしいものと聞こえましたが、それは完全には当てはまりません。マルチスレッド化されたコードには多くの課題があります。

変数の使用

マルチスレッディングの最大かつ最も明らかな課題の1つは、引数として渡さない限り変数を共有できないことです。ただし、同期されたハッシュテーブルという例外がありますが、それについては別の機会に議論します。

PSJobsとランスペースの両方は既存の変数にアクセスせず、異なるランスペースで使用される変数とのやり取りはできません。

これは、これらのジョブに情報を動的に渡す際に大きな課題となります。どの種類のマルチスレッディングを使用しているかによって、答えは異なります。

PoshRSJobモジュールのStart-JobおよびStart-RSJobには、ArgumentListパラメータを使用して、オブジェクトのリストを提供し、それらがリストに表示される順序でスクリプトブロックにパラメータとして渡すことができます。以下に、PSJobとRSJobに使用されるコマンドの例を示します。

PSJob:

Start-Job -Scriptblock {param ($Text) Write-Output $Text} -ArgumentList "Hello world!"

RSJob:

Start-RSJob -Scriptblock {param ($Text) Write-Output $Text} -ArgumentList "Hello world!"

ネイティブのランスペースでは同じような簡単さは得られません。代わりに、PowerShellオブジェクト上でAddArgument()メソッドを使用する必要があります。以下に、それぞれの例を示します。

ランスペース:

$Runspace = [runspacefactory]::CreateRunspace()
$PowerShell = [powershell]::Create()
$PowerShell.Runspace = $Runspace
$Runspace.Open()
$PowerShell.AddScript({param ($Text) Write-Output $Text})
$PowerShell.AddArgument("Hello world!")
$PowerShell.BeginInvoke()

ランスペースプールは同じように動作しますが、以下にランスペースプールに引数を追加する例を示します。

$MaxThreads = 5
$RunspacePool = [runspacefactory]::CreateRunspacePool(1, $MaxThreads)
$PowerShell = [powershell]::Create()
$PowerShell.RunspacePool = $RunspacePool
$RunspacePool.Open()
$PowerShell.AddScript({param ($Text) Write-Output $Text})
$PowerShell.AddArgument("Hello world!")
$PowerShell.BeginInvoke()

ログ

マルチスレッド処理は、ログ記録に関する課題も引き起こします。各スレッドは独立して動作しているため、すべてのスレッドが同じ場所にログを記録することはできません。たとえば、複数のスレッドでファイルにログを記録しようとする場合、1つのスレッドがファイルに書き込んでいる間、他のスレッドは書き込むことができません。これにより、コードの実行が遅くなったり、完全に失敗したりする可能性があります。

例として、以下のコードは、5つのスレッドを使用して100回ログを1つのファイルに記録しようとします。

$RunspacePool = [runspacefactory]::CreateRunspacePool(1, 5)
$RunspacePool.Open()
1..100 | Foreach-Object {
	$PowerShell = [powershell]::Create().AddScript({'Hello' | Out-File -Append -FilePath .\Test.txt})
	$PowerShell.RunspacePool = $RunspacePool
	$PowerShell.BeginInvoke()
}
$RunspacePool.Close()

出力からはエラーは表示されませんが、テキストファイルのサイズを見ると、すべての100のジョブが正常に完了していないことがわかります。

Get-Content -Path .\Test.txt).Count” class=”wp-image-3241″/>
(Get-Content -Path .\Test.txt).Count

これを回避する方法の一つは、別々のファイルにログを記録することです。これにより、ファイルロックの問題は解消されますが、それによって発生したすべての出来事を把握するためには、多くのログファイルを整理する必要があります。

別の選択肢は、出力のタイミングをずらすことを許可し、ジョブが完了した後にのみログを記録することです。これにより、すべての作業を元のセッションを通じて直列化することができますが、すべての詳細がわからないため、順序もわからなくなります。

概要

マルチスレッドは大きなパフォーマンスの向上をもたらす場合もありますが、頭痛の原因にもなる場合もあります。一部のワークロードでは大幅な利益が得られるかもしれませんが、他の場合はまったく利益が得られないかもしれません。マルチスレッドを使用する利点と欠点は多岐にわたりますが、正しく使用すれば、コードの実行時間を大幅に短縮することができます。

さらに読む

Source:
https://adamtheautomator.com/powershell-multithreading/