【疑似マルチスレッド】WScript.Shell.Exec を並列起動して複数タスクを並行処理するタスクランナーの構築
開発現場で自動化スクリプトを組んでいると、必ずと言っていいほどこの壁にぶつかる。
「重たいデータ処理スクリプトや外部コマンドを、1つずつ順番に実行しているせいで、終わる頃には昼休みが明けている」
VBScriptは、その歴史と構造上、完全なシングルスレッドで動作する。`WScript.Shell` の `Run` メソッドを使えば外部プロセスを起動できるが、デフォルトでは同期実行(タスクの完了をその場で待ち続ける)になるため、複数タスクの並行処理など不可能だと諦めていないだろうか。
だが、待ってほしい。
`WScript.Shell` の `Exec` メソッド を正しく理解し、プロセスオブジェクトのライフサイクルをコントロールすれば、VBScriptであっても極めて堅牢な「非同期マルチタスクランナー」を構築できる。
今回は、実務の現場でそのまま使える、複数プロセスの並列起動・終了監視・エラーハンドリングを網羅したプロダクションコードを伝授しよう。
—
なぜ `Run` ではなく `Exec` なのか?
多くの初学者は、外部プロセスの起動に `WScript.Shell.Run` を選ぶ。しかし、`Run` はプロセスIDやストリームをまともに制御できない。「ただ実行して待つ」か「非同期で投げて野放しにする(プロセスの終了検知が困難)」の二者択一だ。
一方、`Exec` メソッドは実行時に `WshProcess` オブジェクト を返す。このオブジェクトが持つプロパティとメソッドこそが、VBScriptによる並列処理の生命線となる。
- `Status` プロパティ: プロセスが実行中(`0`)か終了した(`1`)かをリアルタイムでポーリングできる。
- `StdOut` / `StdErr`: 標準出力・標準エラーをキャプチャし、ログファイルへの書き出しや結果の検証が可能になる。
- `ProcessID`: OSレベルでのプロセス管理が可能になる。
つまり、`Exec` で複数のプロセスを次々とバックグラウンド起動し、メインスレッド側で `Status` を監視し続けるループを回すこと――これが、VBScriptにおける「疑似マルチスレッド」の正体である。
—
実務で直面する3つの罠と設計方針
現場で動くタスクランナーを作るには、単にプロセスを同時に起動するだけでは不十分だ。以下の3点を考慮しなければ、すぐに現場で破綻する。
1. CPUの焼き付き(ビジーウェイト)対策
- 終了監視のループ内で `Status` を無制限に高速ポーリングすると、CPU使用率が100%に張り付き、PCがフリーズする。`WScript.Sleep` を挟んでCPUを適度に解放せよ。
2. 標準ストリームのデッドロック回避
- `Exec` で起動したプロセスが大量のエラー出力や標準出力を吐き出し、VBScript側がそれを読み取らずに放置すると、OSのパイプバッファが溢れてプロセスが永久に停止する(デッドロック)。今回は「ログファイルへ直接リダイレクトするコマンド」を前提とするか、適切なストリーム消化設計にする必要がある。
3. 例外処理とゾンビプロセスの抑制
- スクリプトが途中で異常終了した際、バックグラウンドで起動した子プロセスが残骸(ゾンビプロセス)として居座り続けることがある。確実なクリーンアップ処理が必須だ。
—
プロダクションコード:並列タスクランナー
以下のスクリプトは、複数のコマンド(今回は例として `ping` やバッチファイル、あるいは別のVBScriptなど)を配列で定義し、指定した最大同時実行数…ではなく、一斉起動して全ての完了をスマートに同期・監視する堅牢なタスクランナーの実装である。
‘ ==============================================================================
‘ Script Name: ParallelTaskRunner.vbs
‘ Description: WScript.Shell.Exec を用いた複数プロセスの並列実行・終了監視ランナー
‘ Author: Chief Automation Architect
‘ ==============================================================================
Option Explicit
‘ メイン処理の実行
Main
Sub Main()
Dim objShell, colProcesses, colCommands
Dim i, allFinished, activeCount
Dim startTime
startTime = Timer
WScript.Echo “=== パラレル・タスクランナー起動 ===”
Set objShell = CreateObject(“WScript.Shell”)
‘ ————————————————————————-
‘ 1. 並列実行するコマンドの定義(実務ではここに重いスクリプトやバッチを指定)
‘ ————————————————————————-
colCommands = Array( _
“cmd.exe /c echo Task 1 Started & timeout /t 3 >nul & echo Task 1 Finished”, _
“cmd.exe /c echo Task 2 Started & timeout /t 5 >nul & echo Task 2 Finished”, _
“cmd.exe /c echo Task 3 Started & timeout /t 2 >nul & echo Task 3 Finished” _
)
Dim totalTasks
totalTasks = UBound(colCommands) + 1
‘ プロセスオブジェクトを格納する動的配列
ReDim colProcesses(UBound(colCommands))
‘ ————————————————————————-
‘ 2. 全タスクの非同期一斉起動 (Exec)
‘ ————————————————————————-
WScript.Echo “[” & FormatDateTime(Now, 4) & “] ” & totalTasks & ” 件のタスクを並列起動します…”
For i = 0 To UBound(colCommands)
‘ ExecによりWshProcessオブジェクトを取得し、配列に保持
Set colProcesses(i) = objShell.Exec(colCommands(i))
WScript.Echo ” -> 起動中: [PID: ” & colProcesses(i).ProcessID & “] ” & colCommands(i)
Next
‘ ————————————————————————-
‘ 3. プロセス監視ループ(ポーリング)
‘ ————————————————————————-
WScript.Echo “[” & FormatDateTime(Now, 4) & “] 全タスクの完了を監視中…”
Do
allFinished = True
activeCount = 0
For i = 0 To UBound(colProcesses)
‘ Status = 0: 実行中, Status = 1: 終了
If colProcesses(i).Status = 0 Then
allFinished = False
activeCount = activeCount + 1
End If
Next
‘ 全プロセスが終了していればループ脱出
If allFinished Then Exit Do
‘ CPU使用率の高騰を防ぐため、100ms(0.1秒)スリープを入れる
WScript.Sleep 100
Loop
‘ ————————————————————————-
‘ 4. 結果集計とクリーンアップ
‘ ————————————————————————-
WScript.Echo “————————————————–”
WScript.Echo “全タスクが完了しました。処理時間: ” & FormatNumber(Timer – startTime, 2) & ” 秒”
WScript.Echo “— 実行結果ステータス —”
For i = 0 To UBound(colProcesses)
WScript.Echo ” Task ” & (i + 1) & ” ExitCode: ” & colProcesses(i).ExitCode
Next
WScript.Echo “==================================================”
‘ オブジェクトの解放
Set objShell = Nothing
For i = 0 To UBound(colProcesses)
Set colProcesses(i) = Next
Next
End Sub
—
コードの深掘りとアーキテクチャの解説
1. `ReDim` と `WshProcess` 配列の管理
VBScriptでは、プロセスごとに異なるライフサイクルを持つため、配列でオブジェクトを保持することが不可欠だ。`Set colProcesses(i) = objShell.Exec(…)` によって生成されたプロセスは、スクリプトが終了するまでメモリ上に保持され、その状態を監視し続けることができる。
2. `ExitCode` による成否判定
プロセスの `Status` が `1`(終了)になった後、`colProcesses(i).ExitCode` を参照することで、外部コマンドが正常終了(通常は `0`)したのか、エラー落ちしたのか(非ゼロ)を正確に拾うことができる。これにより、「どのタスクが失敗したか」を特定したロギングやリトライ制御が可能になる。
3. ポーリング間隔(`WScript.Sleep 100`)の妙
`WScript.Sleep 100` は、CPUの無駄な浪費を防ぐための防壁である。このスリープを削ると、VBScriptを実行しているホストプロセス(`cscript.exe` または `wscript.exe`)が単一コアのCPUを100%食い潰し、マシンのパフォーマンス全体を著しく低下させる。実務においては「処理速度のリアルタイム性」と「マシンの負荷」のバランスを見て `100` から `500` ミリ秒程度に設定するのがベストプラクティスだ。
—
実務でさらに堅牢性を高めるためのアドバイス
- CScript での実行を強制する
この手のタスクランナーやCLIツールは、GUIの `wscript.exe` ではなく、必ずCUIの `cscript.exe` で実行すること。`WScript.Echo` の出力結果が標準出力に正しく流れるため、バッチファイルやタスクスケジューラからのキックが劇的に安定する。
(実行コマンド例: `cscript //nologo ParallelTaskRunner.vbs`)
- データベースやファイル連携への応用
もし複数タスクがそれぞれ異なるCSVファイルを処理するものであるなら、タスクごとに処理対象のファイルパスを動的に生成してコマンドに渡せば、巨大なデータ群の並列バッチ処理基盤がVBScript単体で完成する。
VBScriptはレガシーな言語と揶揄されることもあるが、OSの奥底を叩くWSHの機能(特に `Exec`)を正しく理解していれば、今日からでも現場の業務効率を何倍にも跳ね上げる強力な武器となる。
退屈で非効率な順次処理とは、今日でサヨナラしよう。
