更新履歴(初版のみ・2026年07月25日公開)
- 初版公開
この記事を引用する(DOI: 10.5281/zenodo.21547437)
この記事はZenodoにアーカイブされています。常に最新版へ解決されるDOIと、いま表示している版に固定されたDOIの両方を下に示します。
小村 豪(2026)「PowerShellの並列処理 ── ForEach-Object -Parallelとジョブの使い分け」合同会社小村ソフト. https://doi.org/10.5281/zenodo.21547437
- DOI(最新版)
- 10.5281/zenodo.21547437
- DOI(この版)
- 10.5281/zenodo.21547438
「200台のPCにpingを打って死活確認するスクリプトが、1周するのに15分かかる」「共有フォルダー配下10万ファイルのハッシュ計算が終わらない」── PowerShellの自動化がある程度育つと、必ず処理時間の壁にぶつかります。そして、こうした処理の大半は待ち時間が支配的です。CPUは暇なのに、ネットワークの応答やディスクI/Oを1件ずつ順番に待っているせいで遅い。ここが並列化の出番です。
PowerShell 7では ForEach-Object -Parallel が使えるようになり、並列処理のハードルは劇的に下がりました。ただし、雑に並列化すると速くならないどころか遅くなり、しかも結果が壊れるという厄介さもあります。「共有変数を更新したら値が飛んだ」「出力の順序が毎回違う」「呼び出し元で定義した関数が見つからない」は、いずれも並列処理の仕組みを知らないまま使ったときの典型的な症状です。
この記事では、PowerShellで使える並列化の手段を整理し、ForEach-Object -Parallel の使い方と落とし穴、Start-ThreadJob / Start-Job との使い分け、そして「並列化してはいけないケース」までを、実務で判断できる形にまとめます。
1. まず結論
ForEach-Object -ParallelはPowerShell 7.0で追加されました。Windows PowerShell 5.1には存在しません。1- 各スクリプトブロックは別のランスペース(実行環境)で動きます。呼び出し元の変数・関数はそのままでは見えません。変数は
$using:スコープ修飾子で渡します。1 -ThrottleLimitの既定値は5です。PowerShell 7.1以降はランスペースプールが再利用され、ThrottleLimitがそのプールサイズになります。毎回新規に作りたいときは-UseNewRunspaceです。1$using:で渡すのは「参照」です。読むだけなら安全ですが、更新するならSystem.Collections.Concurrentのスレッドセーフな型を使う必要があります。通常のハッシュテーブルやListの同時更新は壊れます。1- 並列化は「必ず速くなる」施策ではありません。公式も、些細な処理を並列化すると通常よりずっと遅くなり得ると明記しています。効くのは待ち時間が長い処理と、マルチコアで意味のある計算処理です。1
- 出力もエラーも順序は不定です。エラーストリームへの書き込み順もランダムで、スクリプトブロック内の終了エラーはそのイテレーションだけを止め、
PSTaskExceptionとして報告されます。1 -AsJobでジョブとして投げられます。ただし-ThrottleLimitはジョブ1つあたりの並列数なので、ジョブを複数作れば同時実行数は掛け算になります。1- 軽量な並列ならStart-ThreadJob、隔離が必要ならStart-Jobです。Start-Jobは別プロセスで動くためオーバーヘッドが大きく、結果はシリアライズされてメソッドを失った状態で戻ります。23
- 複数台のWindowsに同じ処理を流すなら、
Invoke-Commandの暗黙の並列実行が最短です。こちらは既定で32台まで同時実行します。4
2. 並列化の4つの選択肢
まず地図を持っておきます。PowerShellで「同時に走らせる」手段は大きく4つです。
| 手段 | 実行単位 | 使えるバージョン | 向いている用途 |
|---|---|---|---|
ForEach-Object -Parallel |
スレッド(ランスペース) | PowerShell 7.0+1 | コレクションの各要素に同じ処理。第一候補 |
Start-ThreadJob |
スレッド(ランスペース) | PowerShell 7同梱 / 5.1はGalleryから導入2 | 少数の処理を投げっぱなしにして後で回収 |
Start-Job |
別プロセス | 5.1・7とも標準3 | 隔離が必要、または別プロセスにしたい処理 |
Invoke-Command -ComputerName |
リモートの各PC | 5.1・7とも標準4 | 複数台のWindowsに同じ処理を配る |
実務上の使い分けは単純です。同じ処理を大量の対象に適用するなら ForEach-Object -Parallel、対象が「複数のリモートPC」ならリモート実行が先です。後者は指定した複数コンピューターに対して既定で32台まで同時に実行するため、自分で並列化を書く必要がありません。4 リモート実行そのものの設定は「PowerShell Remoting(WinRM)入門」を参照してください。
Start-Job が重いのは、バックグラウンドジョブが別プロセスとして起動されるためです。プロセス起動のコストに加えて、結果はシリアライズされて戻るので、受け取ったオブジェクトはメソッドを持たない「デシリアライズされたコピー」になります。3 一方 Start-ThreadJob は同一プロセス内のスレッドで動くため大幅に軽量です。2
3. ForEach-Object -Parallel の基本
最小の形はこうです。$_ が現在の入力オブジェクト、外の変数は $using: を付けて参照します。1
$timeout = 2 # 呼び出し元の変数
$result = $computers | ForEach-Object -Parallel {
$name = $_
# 外側の変数は $using: を付けないと見えない
$ok = Test-Connection -TargetName $name -Count 1 -TimeoutSeconds $using:timeout -Quiet
# 集計用の共有変数を更新するのではなく、「出力する」のが基本形
[pscustomobject]@{
Computer = $name
Alive = $ok
CheckedAt = Get-Date
}
} -ThrottleLimit 20
$result | Where-Object { -not $_.Alive } | Format-Table
ここで押さえるべき設計原則が一つあります。共有変数を更新するのではなく、各スクリプトブロックが結果を「出力」し、呼び出し元でまとめて受け取る。この形にしておけば、スレッド安全性の問題はそもそも発生しません。並列処理でトラブルになるコードは、たいてい共有状態を書き換えようとしています。
どうしても共有コレクションに集めたい場合は、スレッドセーフな型を使います。1
# 公式が例示しているパターン: ConcurrentDictionaryは複数スレッドから安全に更新できる
$safeDict = [System.Collections.Concurrent.ConcurrentDictionary[string,object]]::new()
Get-Process | ForEach-Object -Parallel {
$dict = $using:safeDict
# TryAdd()はbooleanを返す。捨てないと True/False が出力に混ざる
$null = $dict.TryAdd($_.ProcessName, $_)
}
# 【危険】通常のHashtableやList<T>を$using:で渡して更新するのは安全ではない
# $shared = @{} # 同時更新で内部構造が壊れ、例外や値の消失が起きる
4. 5つの落とし穴
(1) 呼び出し元の関数が見えない
並列スクリプトブロックは別のランスペースで実行されるため、呼び出し元で定義した関数はそのままでは呼べません。共通処理はモジュール化してスクリプトブロックの先頭でインポートするのが正攻法です。
$items | ForEach-Object -Parallel {
Import-Module 'D:\Scripts\Modules\KsOps' -ErrorAction Stop # 各ランスペースで読み込む
Convert-KsRecord -Input $_
} -ThrottleLimit 8
ただし各ランスペースでモジュールを読み込むコストが並列数だけ発生します。重いモジュールを使う処理では、並列度を上げても頭打ちになる主因がここになることがあります。モジュール化の作法は「PowerShellの引数設計とモジュール化」にまとめています。
(2) ランスペースが再利用されるため、状態が持ち越されることがある
PowerShell 7.0では反復ごとに新しいランスペースが作られていましたが、7.1以降は既定でランスペースプールから再利用されます。1 性能上は大きな改善ですが、あるイテレーションで設定した環境設定変数やカレントディレクトリの変更、読み込んだモジュールの状態が、同じランスペースを使う後続のイテレーションから見えることを意味します。イテレーション間の完全な独立が必要なら -UseNewRunspace を指定します(その分は遅くなります)。1
(3) 出力もエラーも順序が保証されない
並列実行の順序は非決定的です。エラーストリームに書かれる順もランダムで、警告・詳細・情報ストリームも同様です。1
1..5 | ForEach-Object -Parallel {
if ($_ -eq 3) { throw "Terminating Error: $_" } # このイテレーションだけが止まる
"Output: $_"
}
# Output: 1 / Output: 4 / Output: 2 / Output: 5 (順不同)、Output: 3 は出ない
スクリプトブロック内の終了エラーはそのイテレーションだけを終了させ、他の並列実行は続行します。エラーは FullyQualifiedErrorId が PSTaskException のErrorRecordとしてエラーストリームに書かれます。「1件でも失敗したら全部止める」という挙動ではないので、全件の成否を自分で集計する必要があります。1
$results = $files | ForEach-Object -Parallel {
# catchブロックの中では $_ がErrorRecordに変わるため、
# 入力オブジェクトは必ず別の変数に退避してから使う
$file = $_
try {
$hash = Get-FileHash -Path $file.FullName -Algorithm SHA256 -ErrorAction Stop
[pscustomobject]@{ Path = $file.FullName; Hash = $hash.Hash; Error = $null }
}
catch {
# 失敗も「出力」として返し、呼び出し元で仕分ける
[pscustomobject]@{ Path = $file.FullName; Hash = $null; Error = $_.Exception.Message }
}
} -ThrottleLimit 8
$failed = $results | Where-Object Error
if ($failed) { Write-Warning "$($failed.Count)件が失敗しました" }
(4) PipelineVariableが使えない
共通パラメーターの -PipelineVariable は、$using: を付けても並列シナリオではサポートされません。1 逐次処理から移植するときに引っかかるポイントです。
(5) 進捗表示とログが混ざる
複数スレッドから同時に Write-Progress や独自ログを書くと、行が混ざったりファイルが競合したりします。ログはスクリプトブロック内で直接ファイルに追記するのではなく、結果オブジェクトとして返して呼び出し元で1か所から書くのが安全です。出力ストリームの設計は「PowerShellの出力ストリームとログ設計」で扱っています。
5. ThrottleLimitの決め方
-ThrottleLimit は同時に走るスクリプトブロックの数で、既定は5です。1 決め方の目安は処理の性質で分けます。
| 処理の性質 | 目安 | 理由 |
|---|---|---|
| ネットワーク待ち(疎通確認、API呼び出し) | コア数より大きくてよい(20〜50程度から試す) | CPUはほぼ遊んでいる。制約は相手側 |
| ディスクI/O待ち | 8〜16程度から試す | 上げすぎるとランダムアクセスが増えて逆効果。SSD/HDDで差が大きい |
| CPU計算(ハッシュ、圧縮、変換) | 論理コア数程度 | それ以上はコンテキストスイッチの無駄 |
| 相手が業務サーバー・API | 相手の許容量が上限 | レート制限や同時接続上限を超えると障害の加害者になる |
最後の行が実務では最重要です。自分のスクリプトを速くするために業務サーバーを落としては本末転倒なので、社内APIやファイルサーバーを相手にするときは、こちらの並列度を「相手が耐えられる範囲」で決めます。APIのレート制限への対処は「PowerShellでREST APIと連携する」を参照してください。
-AsJob を使う場合の注意も公式に明記されています。ThrottleLimitは ForEach-Object -Parallel 1回あたりの制限であり、ジョブを10個作れば「10 × ThrottleLimit」が同時に走ります。1
6. 並列化しないほうが速いケース
公式ドキュメントは踏み込んだ書き方をしています ── 新しいランスペースは逐次処理に比べて相当なオーバーヘッドがあり、並列スクリプトが些細な処理だと通常よりずっと遅くなり得る、実際に試して効果がある場所を見つけよ、と。1 公式の例にも「これは並列化の非効率な使用例」と明記されたサンプルがあるほどです。
判断基準はシンプルです。
- 1件あたりの処理時間が短い(ミリ秒オーダー) → 並列化しない。文字列処理やハッシュテーブル参照の類は逐次のほうが速い
- 件数が少ない(数十件) → 並列化しない。オーバーヘッドが相対的に大きい
- 1件あたり数百ミリ秒以上の待ちがある → 並列化の価値がある
- CPUを長時間使う計算がある → マルチコアで効く
そして必ず測ってから決めることです。Measure-Command で逐次版と並列版を比べれば数十秒で答えが出ます。測定の作法とPowerShellスクリプトの高速化全般は「PowerShellスクリプトが遅いときに見るところ」にまとめました。
# 逐次と並列を同じ入力で比較する(何度か実行して安定した値を見る)
# 並列側は別ランスペースで動くため、呼び出し元で定義した関数は見えない。
# 比較にならないので、モジュールを読み込むか処理を直接書く
$seq = Measure-Command {
$items | ForEach-Object { Invoke-Work $_ }
}
$par = Measure-Command {
$items | ForEach-Object -Parallel {
Import-Module 'D:\Scripts\Modules\KsOps' # ← これがないとコマンド未検出になる
Invoke-Work $_
} -ThrottleLimit 16
}
'{0:N1}秒 → {1:N1}秒' -f $seq.TotalSeconds, $par.TotalSeconds
7. Start-ThreadJobとStart-Jobの使い分け
ForEach-Object -Parallel は「同じ処理を多数の入力に適用する」形にはまりますが、性質の違う処理を同時に走らせて後で回収したいときはジョブが向きます。
# 別々の処理を同時に走らせ、まとめて待つ(スレッドジョブ = 軽量)
# スレッドジョブも別ランスペースで動くため、呼び出し元の関数は見えない。
# 各ジョブの先頭でモジュールを読み込む(または自己完結したスクリプトブロックにする)
$jobs = @(
Start-ThreadJob -Name 'ADユーザー棚卸' -ScriptBlock {
Import-Module 'D:\Scripts\Modules\KsOps'; Get-KsAdUserReport
}
Start-ThreadJob -Name 'ファイルサーバー容量' -ScriptBlock {
Import-Module 'D:\Scripts\Modules\KsOps'; Get-KsShareUsage
}
Start-ThreadJob -Name 'ライセンス集計' -ScriptBlock {
Import-Module 'D:\Scripts\Modules\KsOps'; Get-KsLicenseSummary
}
)
# 完了を待って結果を回収する。エラーは -ErrorVariable で必ず受け取る
$reports = $jobs | Receive-Job -Wait -ErrorAction SilentlyContinue -ErrorVariable jobErrors
# 状態が Failed になるのは、スクリプトブロックが「終了エラー」で落ちたときだけ。
# Write-Error のような非終了エラーで終わったジョブは Completed のままなので、
# 状態だけを見ていると「エラーが出ていたのに成功扱い」になる
$failed = @($jobs | Where-Object { $_.State -eq 'Failed' })
$jobs | Remove-Job -Force
if ($failed.Count -gt 0 -or @($jobErrors).Count -gt 0) {
throw "並列実行でエラーが発生しました: $(($jobErrors | ForEach-Object { $_.Exception.Message }) -join ' / ')"
}
-AutoRemoveJob をここで使わないのは、回収した直後にジョブが消えてしまい、どれが失敗したのかを確認できなくなるからです。Receive-Job のエラーは既定で非終了エラーなので、何も書かなければ「一部が失敗したのに、成功したぶんだけ返ってきて完了したように見える」という、いちばん困る形になります。棚卸しやレポートの自動化では、この取りこぼしが数字の間違いとして静かに残ります。
Start-Job(プロセス分離型)を選ぶのは、次のような場合です。3
- 呼び出し元プロセスを巻き込みたくない処理(クラッシュしうるネイティブDLLを叩くなど)
- 別のプロセス環境が必要な処理(異なるカルチャ設定や環境変数で動かしたい)
- 32bit / 64bitなど、実行環境そのものを分けたい処理
逆にこれらに当てはまらないなら、オーバーヘッドとシリアライズの制約の分だけ Start-Job は不利です。デシリアライズされたオブジェクトはメソッドを持たない点も、後段の処理で効いてきます。3
8. Windows PowerShell 5.1しかない環境では
5.1で -Parallel は使えません。現実的な選択肢は3つです。
Start-ThreadJob(PowerShell GalleryのThreadJobモジュールを導入)── 5.1でも軽量な並列が使えます。社内配布の作法は「PowerShellモジュールの社内配布と更新」を参照2Invoke-Command -ComputerName── 対象が複数台のWindowsなら、これだけで並列になります(既定32台同時)4- PowerShell 7を導入する ── 5.1と7は共存できるので、重い処理だけ7で動かす選択も現実的です
3番目が結局いちばん安上がりになることが多く、当社でも「並列化のために凝った5.1コードを書く」より「その処理だけ7で動かす」を推奨しています。共存と移行の考え方は「Windows PowerShell 5.1とPowerShell 7の違い」にまとめてあります。
9. 実務の定石(判断表)
| やりたいこと | 選択 | 補足 |
|---|---|---|
| 多数の対象に同じ処理(疎通確認、ハッシュ計算、API呼び出し) | ForEach-Object -Parallel |
PowerShell 7専用。結果は出力で返す設計に1 |
| 複数台のWindowsで同じ処理 | Invoke-Command -ComputerName |
既定32台同時。自前の並列化は不要4 |
| 性質の違う処理を同時実行 | Start-ThreadJob |
軽量。Receive-Job -Wait で回収2 |
| プロセス分離が必要 | Start-Job |
重い。結果はデシリアライズされる3 |
| 結果を1か所に集めたい | 出力で返す / ConcurrentDictionary |
通常のHashtable・Listの同時更新は不可1 |
| 1件が軽い・件数が少ない | 並列化しない | オーバーヘッドで逆に遅くなる1 |
| 相手が業務サーバー・API | ThrottleLimitを相手基準で決める | レート制限・同時接続上限が事実上の上限 |
| イテレーション間の独立が必須 | -UseNewRunspace |
再利用による状態の持ち越しを避ける(遅くなる)1 |
10. まとめ
ForEach-Object -ParallelはPowerShell 7.0以降の機能で、各スクリプトブロックを別ランスペースで実行します。呼び出し元の変数は$using:、関数はモジュール化して読み込むのが基本形です。- 共有変数を更新するのではなく、各イテレーションが結果を出力して呼び出し元で集約する設計にすれば、スレッド安全性の問題はほぼ回避できます。どうしても共有するならConcurrent系の型を使います。
- 既定のThrottleLimitは5。待ち時間が支配的な処理は大きく、CPU処理はコア数程度、相手がある処理は相手の許容量が上限です。
- 出力・エラーの順序は不定で、終了エラーはそのイテレーションだけを止めます。全件の成否は自分で集計してください。
- 並列化は万能ではありません。軽い処理や少件数ではオーバーヘッドで遅くなります。必ず
Measure-Commandで逐次版と比較してから採用してください。 - 5.1環境では
Start-ThreadJob、リモートならInvoke-Command、それでも足りなければPowerShell 7の導入を検討するのが現実的です。
サンプルコードのダウンロード
この記事で扱ったコードは、そのまま動かせる形にまとめて配布しています。ForEach-Object -Parallel / Start-ThreadJob の実務向け定型が入っています。
この記事のサンプルは、PowerShell 7.6 で実際に実行して検証しています(Pester 8件)。zipに含まれる Invoke-SampleTests.ps1 を実行すれば、お手元でも同じ検証を再現できます。
# 構文解析 + 静的解析 + Pesterテスト
./Invoke-SampleTests.ps1
設定値(パス、サーバー名、テナントIDなど)は例です。そのまま本番環境で実行せず、自社の環境に合わせて読み替えてください。
関連記事
- Windows PowerShell 5.1とPowerShell 7の違い ── 社内スクリプト移行の実務ガイド
- PowerShell Remoting(WinRM)入門 ── 複数台のWindowsを一括管理する
- PowerShellの引数設計とモジュール化 ── 「動くスクリプト」から「人に渡せるスクリプト」へ
- PowerShellでファイルサーバーを棚卸しする ── 容量調査とアクセス権(ACL)監査
- PowerShellのエラー処理と再実行設計 ── try/catchが効かない罠からexit code・リトライの定石まで
関連する相談領域
合同会社小村ソフトでは、時間のかかる運用スクリプトの高速化、並列処理を含む自動化設計のレビュー、「並列化したら結果がおかしくなった」といった不具合の調査を扱っています。
参考リンク
-
Microsoft Learn, ForEach-Object. PowerShell 7.0で-Parallelパラメーターセットが追加されたこと、各スクリプトブロックが新しいランスペースで実行されること、$using:スコープ修飾子による変数の受け渡し、-ThrottleLimitの既定値が5でランスペースプールのサイズになること、7.1以降はランスペースが再利用され-UseNewRunspaceで新規作成できること、-AsJobと-TimeoutSecondsの動作、$using:で渡した参照の更新にはSystem.Collections.Concurrentのようなスレッドセーフな型が必要なこと、非終了エラーの出力順が不定であること、終了エラーが個々の並列インスタンスのみを終了させFullyQualifiedErrorIdがPSTaskExceptionになること、PipelineVariableが並列シナリオで未サポートであること、新規ランスペースのオーバーヘッドが大きく些細な処理では逆に遅くなり得ること、-AsJob使用時はThrottleLimitがジョブごとの制限であることについて。 ↩ ↩2 ↩3 ↩4 ↩5 ↩6 ↩7 ↩8 ↩9 ↩10 ↩11 ↩12 ↩13 ↩14 ↩15 ↩16 ↩17 ↩18 ↩19 ↩20 ↩21 ↩22
-
Microsoft Learn, Start-ThreadJob. Start-ThreadJobが別プロセスではなく同一プロセス内の独立したスレッドでスクリプトブロックを実行し、Start-Jobより軽量であること、標準のジョブコマンドレット(Receive-Job等)で扱えること、-ThrottleLimitによる同時実行数の制御について。 ↩ ↩2 ↩3 ↩4 ↩5
-
Microsoft Learn, about_Jobs. バックグラウンドジョブがコマンドを新しいプロセスで非同期に実行すること、Start-Job・Get-Job・Receive-Job・Wait-Jobによるジョブ操作、ジョブの結果がシリアライズを経て返されることについて。 ↩ ↩2 ↩3 ↩4 ↩5 ↩6
-
Microsoft Learn, Invoke-Command. -ComputerNameに複数のコンピューターを指定して同じコマンドを実行できること、-ThrottleLimitが同時接続数を制限し既定値が32であること、-AsJobによるバックグラウンド実行について。 ↩ ↩2 ↩3 ↩4 ↩5
関連する記事
同じタグを共有する最新の記事です。さらに近い話題で知識を深められます。
PowerShellスクリプトが遅いときに見るところ ── 配列・パイプライン・突合の勘所
PowerShellスクリプトが遅い原因の定番を整理します。配列の+=がO(n^2)になる理由、パイプラインとforeachの差、突合のハッシュテーブル化、ファイルI/Oの改善、そして正しい測り方までを実務目線で解説します。
Write-Hostをやめる ── PowerShellの出力ストリームとログ設計
PowerShellの6つの出力ストリームの使い分け、Write-Hostが抱える問題と正しい使いどころ、関数の戻り値が汚れる原因、-Verboseや-InformationVariableによる呼び出し側制御、構造化ログの残し方を整理します。
PowerShellから外部exeを正しく呼ぶ ── 引数のクォート・終了コード・文字化けの落とし穴
PowerShellからrobocopyや社内EXEを呼ぶと引数が壊れる、終了コードが取れない、出力が文字化けする。PowerShell 7.3の引数渡し変更、停止解析トークン--%、Start-Processの使い分けまでを実務目線で整理します。
PowerShellでの資格情報の安全な扱い ── 平文パスワードをスクリプトから追放する
PowerShellスクリプトの平文パスワードを安全な保管へ移行する手順を整理します。SecureStringの実像と限界、Export-ClixmlによるDPAPI保存の仕組み、SecretManagement/SecretStoreの使いどころまで解説します。
PowerShellスクリプトの引数設計とモジュール化 ── 「動くスクリプト」から「人に渡せるスクリプト」へ
PowerShellスクリプトを他人に渡せる品質に引き上げる手順を整理します。paramブロックと[CmdletBinding()]、入力検証、パイプライン入力、-WhatIf対応、.psm1モジュール化、社内共有とGit管理の勘所まで解説します。
関連トピック
このテーマと近いトピックページです。記事を起点に、関連するサービスや他の記事へ進めます。
Windows技術トピック
Windows 開発、不具合調査、既存資産活用の技術トピックをまとめた入口です。
このテーマがつながるサービス
この記事は次のサービスページにつながります。近い入口からご覧ください。
Windowsアプリ開発
業務アプリ、装置連携、通信ツールなどの Windows ソフト開発を支援します。
よくある質問
この記事のテーマについて、相談時によくある質問をまとめています。
- ForEach-Object -ParallelはWindows PowerShell 5.1でも使えますか?
- 使えません。-ParallelはPowerShell 7.0で追加されたパラメーターセットで、5.1にはそもそも存在しません。5.1で並列化するなら、PowerShell Galleryから導入できるThreadJobモジュールのStart-ThreadJob、標準のStart-Job(プロセス分離型で重い)、あるいはRunspacePoolを自分で組む方法になります。社内スクリプトの高速化が目的なら、5.1で凝った並列処理を書くより、PowerShell 7を導入して-Parallelを使うほうが保守性の面でも有利です。
- 並列化したのに、かえって遅くなりました。なぜですか?
- 並列実行のオーバーヘッドが処理そのものより大きいためです。ForEach-Object -Parallelは各スクリプトブロックを別のランスペースで実行するので、逐次処理に比べて相当のオーバーヘッドがあり、公式ドキュメントも「並列スクリプトが些細な処理だと通常よりずっと遅くなり得る」と明記しています。1件あたりの処理が数ミリ秒で終わる、あるいは件数が数十件しかないといった場合は、並列化しないほうが速いのが普通です。効果が出るのは、ネットワーク待ちやファイルI/O待ちが長い処理や、マルチコアで意味のある計算処理です。
- $using:で渡した変数に、並列スクリプトブロックの中から値を書き込めますか?
- 参照している値を読むのは安全ですが、書き込みは対象がスレッドセーフな型でない限り安全ではありません。$using:は呼び出し元スレッドから各スクリプトブロックのスレッドへ変数の参照を渡す仕組みで、複数スレッドが同時に触るため、通常のハッシュテーブルやListを更新すると壊れます。集計結果を集めたい場合は、System.Collections.Concurrent名前空間のConcurrentDictionaryやConcurrentBagのようなスレッドセーフな型を使うか、そもそも各スクリプトブロックが値を出力して呼び出し元で受け取る形に設計してください。
- ThrottleLimitはいくつにするのが正解ですか?
- 処理の性質で変わります。既定値は5です。ネットワーク待ちやファイルI/O待ちが支配的な処理(サーバーへの疎通確認、API呼び出しなど)は、CPUコア数より大きい値でも効果が出ます。一方、CPUを使い切る計算処理では、コア数程度を超えると競合で遅くなるだけです。相手が業務サーバーやAPIの場合は、こちらの都合だけでなく相手側の同時接続上限やレート制限も上限になります。まずは既定の5で測り、倍にして効果があるかを確認する、という進め方が安全です。
- 並列スクリプトブロックの中から、自分で書いた関数が呼べません。
- 並列スクリプトブロックは呼び出し元とは別のランスペースで実行されるため、呼び出し元のスコープで定義した関数や変数はそのままでは見えません。対処は2つで、共通処理をモジュール(.psm1)にしてスクリプトブロックの先頭でImport-Moduleする、または関数定義をテキストとして$using:で渡してスクリプトブロック内で再定義する方法です。保守性の観点では前者を推奨します。ただし各ランスペースでモジュールを読み込むコストがかかるので、モジュールが重い場合は並列度を上げても頭打ちになる点に注意してください。
著者プロフィール
記事の著者プロフィールページです。
小村 豪
合同会社小村ソフト 代表
Windows ソフト開発、技術相談、不具合調査を中心に、既存資産が残る案件や原因が見えにくい障害調査に強みがあります。
公開リンク