単一スレッドの限界を突破せよ:大量ファイル処理を加速させる「マルチプロセス・ディスパッチャー」設計論
「VBScriptで10万個のファイルをループ処理させていたら、処理が終わるまでに日が暮れてしまった……」
もし君がそんな経験をしたことがあるなら、君はまだVBScriptという言語の「真の扱い方」を知らない。VBScriptは、設計思想そのものがシングルスレッドだ。一つの処理が終わるまで、次の処理は待たされる。これは、現代のマルチコアCPU環境においては、計算リソースの致命的な無駄遣いに他ならない。
大量のファイルをFSO(FileSystemObject)で操作する際、我々に求められるのは「単なるループ」ではない。「いかにして計算資源を並列化し、スループットを最大化するか」というアーキテクチャの設計だ。
本稿では、単一のVBScriptの限界を突破し、複数の`cscript.exe`プロセスを並行起動させることで、処理時間を劇的に短縮する「マルチプロセス・ディスパッチャー・パターン」を伝授する。
1. なぜ「単一ループ」は敗北するのか
業務自動化の現場では、数万、数十万というファイルに対して「移動」「リネーム」「内容解析」といった重いFSO操作を行う場面が多々ある。
通常のプログラマはこう書く。
1. 全ファイルを取得する
2. `For Each` で一つずつ処理する
これでは、CPUのコアが1つしか働いていない間、他のコアは遊んでいる。さらに、FSOのメソッド(`File.Move`や`Folder.Create`など)はOSのI/O待ちが発生する。この「待ち時間」を有効活用できない設計は、プロフェッショナルとは言えない。
我々が目指すべきは、「Dispatcher(司令塔)」と「Worker(実行部隊)」の分離である。
- Dispatcher: 対象ファイルをスキャンし、タスクを複数のグループ(チャンク)に分割。複数の`cscript.exe`を非同期で起動する。
- Worker: Dispatcherから渡された「担当リスト」に基づき、集中的にFSO操作を行う。
これにより、OSレベルでプロセスが分割され、マルチコアによる並列処理が実現する。
2. 堅牢な設計のための3つの鉄則
実装に入る前に、設計の「急所」を叩き込んでおく。ここを疎かにすると、並列処理は「バグの温床」へと変貌する。
① コマンドライン引数の限界を知れ
Windowsのコマンドライン引数には長さの制限がある。数千個のファイルパスを引数として渡そうとするのは素人の発想だ。
正解は、「処理対象のリストを一時ファイル(.txt)に書き出し、そのファイルパスをWorkerに渡す」ことだ。
② 共有リソースの競合(Race Condition)を回避せよ
複数のプロセスが同時に同じログファイルに書き込もうとしたり、同じディレクトリにファイルを作成しようとすると、ファイルロックエラーでプロセスが死ぬ。
- ログ: プロセスごとに個別のログファイルを持たせるか、書き込み時に排他制御を行う設計が必要だ。
- 出力先: Worker間で出力先ディレクトリが重ならないよう、Dispatcherが厳密に管理せよ。
③ ゾンビプロセスの防止
Dispatcherが異常終了した際、Workerだけが生き残り続ける「ゾンビプロセス」が発生することがある。エラーハンドリングを徹底し、親プロセスが死ぬときは子プロセスも制御下に置く意識を持て。
3. 実装:プロダクション・コード・テンプレート
以下に、実務でそのまま使えるレベルの構成を示す。
【Worker.vbs】(実行部隊)
このスクリプトは、渡されたリストファイルを読み込み、FSO操作を完遂する。
' =================================================================
' Worker.vbs: 個別のタスクを実行するワーカープロセス
' 引数: 1st = 処理対象ファイルリストのパス
' =================================================================
Option Explicit
Dim objFSO, objListFile, strLine, strTargetFile
Dim strListPath
' 引数チェック
If WScript.Arguments.Count < 1 Then
WScript.Echo "Error: Task list file path is required."
WScript.Quit 1
End If
strListPath = WScript.Arguments(0)
Set objFSO = CreateObject("Scripting.FileSystemObject")
' リストファイルの存在確認
If Not objFSO.FileExists(strListPath) Then
WScript.Echo "Error: List file not found: " & strListPath
WScript.Quit 1
End If
On Error Resume Next
' リストファイルを一行ずつ読み込んで処理
Set objListFile = objFSO.OpenTextFile(strListPath, 1) ' ForReading
Do Until objListFile.AtEndOfStream
strLine = Trim(objListFile.ReadLine)
If strLine <> "" Then
' --- ここに実際の業務ロジックを記述する ---
' 例: ファイルの拡張子をチェックしてリネームする
If objFSO.FileExists(strLine) Then
' 重い処理のシミュレーション(実際はFSO操作)
' objFSO.MoveFile strLine, objFSO.BuildPath("C:\TargetDir", "new_" & objFSO.GetFileName(strLine))
' ログ出力(簡易版。実際はプロセスごとに分けるのが理想)
' WScript.Echo "Processed: " & strLine
End If
' ---------------------------------------
If Err.Number <> 0 Then
' エラー発生時はログに残して次へ(プロセスを落とさない)
' WScript.Echo "Error processing " & strLine & ": " & Err.Description
Err.Clear
End If
End If
Loop
objListFile.Close
Set objListFile = Nothing
Set objFSO = Nothing
WScript.Quit 0
【Dispatcher.vbs】(司令塔)
全ファイルをスキャンし、プロセス数に応じて分割してWorkerを起動する。
' =================================================================
' Dispatcher.vbs: タスクを分割し、Workerを並列起動する司令塔
' =================================================================
Option Explicit
Dim objFSO, objShell, objFolder, objFile
Dim strSourceDir, strWorkDir, strListPrefix
Dim colFiles, i, intTotalFiles, intChunkSize, intWorkerCount
Dim strListFile, objListFile, strCurrentLine
Dim intProcessID
' --- 設定領域 ---
strSourceDir = "C:\TargetFiles" ' 対象ディレクトリ
strWorkDir = "C:\WorkTemp" ' 一時リスト格納用
strListPrefix = "task_list_" ' リストファイル接頭辞
intWorkerCount = 4 ' 並列プロセス数(CPUコア数に合わせる)
' ----------------
Set objFSO = CreateObject("Scripting.FileSystemObject")
Set objShell = CreateObject("WScript.Shell")
' 作業ディレクトリの準備
If Not objFSO.FolderExists(strWorkDir) Then objFSO.CreateFolder(strWorkDir)
' 1. 対象ファイルの全リストアップ
Set objFolder = objFSO.GetFolder(strSourceDir)
Set colFiles = objFolder.Files
intTotalFiles = colFiles.Count
If intTotalFiles = 0 Then
WScript.Echo "No files found."
WScript.Quit
End If
WScript.Echo "Total files: " & intTotalFiles & " / Workers: " & intWorkerCount
' 2. チャンクサイズの計算
intChunkSize = Application.WorksheetFunction_Ceiling(intTotalFiles / intWorkerCount) ' 概念的な計算
' VBScriptにはCeilingがないので手動計算
intChunkSize = intTotalFiles \ intWorkerCount
If intTotalFiles Mod intWorkerCount <> 0 Then intChunkSize = intChunkSize + 1
' 3. ファイルリストの分割とWorkerの起動
Dim intFileCounter, intCurrentChunkIdx
intFileCounter = 0
intCurrentChunkIdx = 0
' 全ファイルを走査してリストファイルに書き出しながら、一定数ごとにWorkerを起動
' ※メモリ節約のため、一度全リストを作るのではなく、分割しながら処理する設計が望ましい
' ここでは実装の簡潔さのため、一旦全リストをメモリに載せるのではなく、
' ターゲットファイルを順番にリストファイルに書き込んでいく
Dim objTempList
Set objTempList = objFSO.CreateTextFile(strWorkDir & "\" & strListPrefix & intCurrentChunkIdx & ".txt", True)
For Each objFile In colFiles
objTempList.WriteLine objFile.Path
intFileCounter = intFileCounter + 1
' チャンクがいっぱいになったらリストを閉じ、Workerを起動
If intFileCounter >= intChunkSize Or intFileCounter = intTotalFiles Then
objTempList.Close
' Workerの起動 (WshShell.Run で 非同期起動: bWaitOnReturn = False)
' 第2引数 0 はウィンドウを非表示にする
strListFile = strWorkDir & "\" & strListPrefix & intCurrentChunkIdx & ".txt"
WScript.Echo "Launching Worker " & intCurrentChunkIdx & " for " & strListFile
objShell.Run "cscript.exe //nologo """ & WScript.ScriptFullName & "_worker.vbs"" """ & strListFile & """", 0, False
' 次のチャンクへ
intCurrentChunkIdx = intCurrentChunkIdx + 1
intFileCounter = 0
If intCurrentChunkIdx < intWorkerCount Then
Set objTempList = objFSO.CreateTextFile(strWorkDir & "\" & strListPrefix & intCurrentChunkIdx & ".txt", True)
End If
End If
Next
WScript.Echo "All workers dispatched. Monitor the process."
' --- 補足: 本来はここで全プロセスが終了するまで待機するロジックが必要 ---
*(注: 実装の簡略化のため、Worker部分は `Dispatcher.vbs` と別ファイルとして保存するか、判定ロジックを組む必要がある。実務では `Dispatcher.vbs` から `cscript.exe Worker.vbs` を呼び出す形にする。)*
4. プロフェッショナルのための最終チェックリスト
この設計をプロダクション環境に投入する前に、以下の問いに答えられるか確認してほしい。
1. 「I/Oのボトルネック」はどこか?
CPUを増やしても、HDD/SSDの書き込み速度が限界なら意味がない。並列数は、ディスクの性能を見極めて決定せよ。
2. 「エラーログ」は集約されているか?
バラバラに動くプロセスは、エラーが起きた瞬間に「黙って消える」。各Workerがエラーをテキストファイルに即座に書き出す仕組みは必須だ。
3. 「クリーンアップ」は設計されているか?
処理が終わった後、`strWorkDir` に残った大量の `.txt` ファイルを削除する仕組みはあるか?
結論
VBScriptは古い言語だ。しかし、その特性(シングルスレッド)を理解し、OSの機能(マルチプロセス)を呼び出すアーキテクチャを構築できれば、それは強力な武器になる。
「ループを回す」という思考を捨て、「プロセスを配分する」という思考へ。
これこそが、業務自動化エンジニアが到達すべき、極限の知見である。
