がらぱっぱ

自分用覚え書き中心(モバイル関係中心だったはずが)

WSHでフォルダ単位でマルチプロセス(最大同時実行数制限付き)

WSHで並列処理を行いたい場合の方法についての覚書

大量のデータを処理する場合、CPU使用率やディスクに余裕がある時には並列処理を行うのが高速化の方法です。

しかしながら、際限なく処理が並列に動くとかえって遅くなってしまいます。

なので一般的なマルチスレッド処理の場合、同時起動数を制限できるようになっているのですが、WSHの場合これを実装しなければなりません。

 

やりたいこと

  1. フォルダ単位で並列起動。フォルダ以下のファイルに対して処理。
  2. 最大起動数を指定できるようにする
  3. 子プロセスの返却値を取り出せるようにする

方針

  • 子プロセスの返却値を取り出せるようにするために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あたりは動的配列とか割と苦手なので、最大起動数分の配列を用意して毎回配列をなめる方式とします。

'Global Valiables
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で出力)を親プロセス側に取り込みます。

'Constants
MAX_PROCESS = 2                         '最大起動数

WshFinished = 1                         'ジョブの実行が完了しました。

'Global Valiables
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

'*******************************************************************************
' 子プロセスの終了チェック
'   終了しているプロセスがないかをチェックする。
'   起動中のプロセスがある場合はFalseを返す
'*******************************************************************************
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()
    ' 起動可能かをProcessArrayから見つける
    For i = LBound(ProcessArray) To UBound(ProcessArray)
      'プロセス起動可能(最大数以下)であれば起動
      If IsEmpty(ProcessArray(i)) Then
        If NotExec Then
          ExecStr = "cscript //Nologo " & WScript.ScriptName & " """ & filePath & """ " & numProcess
'          WScript.Echo ExecStr   'for debug
          Set ProcessArray(i) = WshShell.Exec(ExecStr)
          numProcess = numProcess + 1
          NotExec = False
          Exit For
        End If
      End If
    Next
    If NotExec Then
'      WScript.Sleep 100
    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                    'わかりやすくするために1秒待つ
  '**↑フォルダ単位の処理ここまで**
End Function

'子プロセス終了監視(呼び出し側処理)
'  子プロセスの出力結果を処理するユーザ処理を記載する。
Sub checkChildProcess(sOutPut)
  '**↓子プロセスの結果に対しての処理記述**
  WScript.echo sOutPut					'ここでは出力結果をそのままコンソールに出力する
  '**↑子プロセスの結果処理ここまで**
End Sub