WSHで並列処理を行いたい場合の方法についての覚書
大量のデータを処理する場合、CPU使用率やディスクに余裕がある時には並列処理を行うのが高速化の方法です。
しかしながら、際限なく処理が並列に動くとかえって遅くなってしまいます。
なので一般的なマルチスレッド処理の場合、同時起動数を制限できるようになっているのですが、WSHの場合これを実装しなければなりません。
やりたいこと
- フォルダ単位で並列起動。フォルダ以下のファイルに対して処理。
- 最大起動数を指定できるようにする
- 子プロセスの返却値を取り出せるようにする
方針
- 子プロセスの返却値を取り出せるようにするためにWScript.Shell.execを利用する
- 最大起動数のコントロールのために現在起動中のプロセスを覚えておく入れ物を用意
- スクリプトファイルは1つで親と子を引数で切り分けるようにする
親の場合の引数は対象フォルダ、子の場合の引数は対象フォルダと子であるフラグ
必要そうなリファレンス
子プロセスの実行
Exec メソッド | Microsoft Docs
Execで実行された子コマンドシェルの出力内容をStdInストリーム経由で取得できる。
wscriptから実行するとコマンドプロンプトの黒いやつが出てくる。
RunメソッドだとintWindowStyle引数で表示・非表示が指定できるのだが、Execメソッドだとcscriptの場合は別ウィンドウが表示されない、wscriptの場合は別ウインドウが表示されるという動作になるようです。
子プロセスの出力結果の読み込み
ReadAll メソッド (Windows Script Host) | Microsoft Docs
Execで実行された子コマンドシェルの出力内容を読み込む。
このメソッド呼び出すと子コマンドシェルが終わるまで待つこととなるので注意。
ファイルの一覧
Files プロパティ | Microsoft Docs
SubFolders プロパティ | Microsoft Docs
処理のポイント
非同期処理起動
非同期で処理を行う方法ですが、自分自身をExecメソッドで呼び出します。
呼び出し側の処理(エッセンス
Set WshShell = CreateObject("WScript.Shell")
ExecStr = "cscript //Nologo " & WScript.ScriptName & " """ & filePath & """ " & numProcess
Set oExec = WScriptShell.Exec(ExecStr)
自分自身(WScript.ScriptName)をcscriptで呼び出します。この時ロゴなしとする(//Nologo)。
第一引数はファイルパスですが、スペースが入る可能性があるためダブルクォーテーションで括ります。
第二引数は並行処理で起動されたか否かを識別するフラグです。
ここでは起動された総プロセス数をカウントしていてその値を渡しています。
最大起動数の制御と現在起動中のプロセス保持
複数起動しているプロセスのプロセス情報(WshScriptExec )を保持し、終了監視を行う必要があります。
VBSあたりは動的配列とか割と苦手なので、最大起動数分の配列を用意して毎回配列をなめる方式とします。
ReDim ProcessArray(MAX_PROCESS-1)
Function checkSubProcess()
For i = LBound(ProcessArray) To UBound(ProcessArray)
If Not IsEmpty(ProcessArray(i)) Then
If ProcessArray(i).Status = WshFinished Then
ProcessArray(i) = Empty
End If
End If
Next
End Function
呼び出し側処理と子プロセス側処理の切り分け
引数(第二引数)の有無によって判断します。
親プロセス呼び出し
MultiProcess.vbs フォルダパス
子プロセス呼び出し
MultiProcess.vbs フォルダパス 起動プロセス数
という呼び出し方を前提に、引数が2個の場合は子プロセス、それ以外は親プロセスとみなして処理を分岐します。
ということでソース
は以下になります。
指定したフォルダ以下のフォルダを探して子プロセスとして起動(フォルダ単位の処理)
また、最大起動数まで起動された場合は現在起動中のプロセスが終わるまで待ちます。
最後、すべての起動中プロセスが終わるまで待って完了。
子プロセスの出力結果(WScript.Echoで出力)を親プロセス側に取り込みます。
MAX_PROCESS = 2
WshFinished = 1
ReDim ProcessArray(MAX_PROCESS-1)
numProcess = 0
Set fso = CreateObject("Scripting.FileSystemObject")
Set WshShell = CreateObject("WScript.Shell")
If WScript.Arguments.count = 0 Then
TargetDir = INputBox("フォルダ名を入れてください。")
Else
TargetDir = WScript.Arguments(0)
End If
Set oFolder = fso.GetFolder(TargetDir)
If WScript.Arguments.count <> 2 Then
Call processFolder(oFolder.Path)
Call waitSubProcess
MsgBox "完了"
WScript.Quit
Else
processChildProcess(TargetDir)
End If
Function checkSubProcess()
checkSubProcess = false
For i = LBound(ProcessArray) To UBound(ProcessArray)
If Not IsEmpty(ProcessArray(i)) Then
If ProcessArray(i).Status = WshFinished Then
sOutPut = ProcessArray(i).StdOut.ReadAll
Call checkChildProcess(sOutPut)
ProcessArray(i) = Empty
Else
checkSubProcess = True
End If
End If
Next
End Function
Function execSubProcess(filePath)
NotExec = True
While NotExec
Call checkSubProcess()
For i = LBound(ProcessArray) To UBound(ProcessArray)
If IsEmpty(ProcessArray(i)) Then
If NotExec Then
ExecStr = "cscript //Nologo " & WScript.ScriptName & " """ & filePath & """ " & numProcess
Set ProcessArray(i) = WshShell.Exec(ExecStr)
numProcess = numProcess + 1
NotExec = False
Exit For
End If
End If
Next
If NotExec Then
End If
Wend
End Function
Sub waitSubProcess()
While checkSubProcess()
WScript.Sleep 100
Wend
End Sub
Sub processFolder(folderPath)
Set oSubFolder = fso.GetFolder(folderPath)
For Each sFolder In oSubFolder.subFolders
Call execSubProcess(sFolder.Path)
Call processFolder(sFolder.Path)
Next
End Sub
Function processChildProcess(sFolder)
WScript.echo TargetDir
Set oSubFolder = fso.GetFolder(sFolder)
For Each oFile In oSubFolder.Files
WScript.echo " " & oFile.name
Next
WScript.sleep 1000
End Function
Sub checkChildProcess(sOutPut)
WScript.echo sOutPut
End Sub