RunspacePoolで始めるPowerShellの非同期処理

**PowerShell2.0系をメインの対象にした記事ですが、バージョンも技術も既にレガシーなものだと思っているので、この記事は参考程度にしてください。**

仕事でVMware vSphere上で行う作業を自動化したいと思い、vmware PowerCLI、PowerShellに手を伸ばした。
自動化をすすめる上で避けられない(?)のが非同期処理（並列処理やスレッド処理？とも聞く）。

PowerShellには2.0や3.0系のバージョン以降、WorkFlowや複数Job実行系のコマンドレットが充実してきた。
今回は_Windows Server 2008R2_系サーバで運用している業務環境を対象に、何もしていなければデフォルトのバージョン2.0で動作する**RunspacePool**を使った並列処理について考えてみる。


#サンプルコードについて説明
まずは簡単に仕組みを理解するために、
このコードは一度に以下のプロセスを実行する。
* 10秒スリープ
* 指定したフォルダに空ファイルを２つ作成

通常であれば**10秒スリープした後に空ファイルを1つずつ作成**か、
**空ファイルを1つずつ作成した後に10秒スリープ**するが、以下のコードは同時にすべて行う事を期待するシンプルなものである。

なお、以下のコードはほとんどこちらに書いてあるものを自分でやってみたいもの向けに書きなおしました。（スクリプトブロックによる記述方法とか・・・)
* [RunspacePoolを使って、PowerShellを非同期実行](https://www.gmo.jp/report/single/?art_id=195 "RunspacePoolを使って、PowerShellを非同期実行")
* [PowerShell による同期処理、非同期処理、並列処理 を考えてみる](http://tech.guitarrapc.com/entry/2013/10/29/100946)
大変参考になりました。ありがとうございます。


##コード
```ps1:runspaceTest.ps1
function RunspaceTest{ 
    try
    {
        # RunspacePoolの準備
        $maxRunspace = 3 #一度にオープン出来るRunspaceの数。今回は定数だが、たいていはオブジェクトや配列の数で流す。
        $runspacePool = [RunspaceFactory]::CreateRunspacePool(1, $maxRunspace) # CreateRunspacePool(最小値,最大値)
        $runspacePool.Open() # open pool
        
        #PowerShellオブジェクトと非同期処理結果格納用のArrayList。
        $aryPowerShell = New-Object System.Collections.ArrayList
        $aryIAsyncResult = New-Object System.Collections.ArrayList

        # 一括でRunspacePoolに投げる処理
        for($i=0; $i -lt $maxRunspace - 1; $i++)
        { 
            #複数処理実行時用のスクリプトブロック。
            $command = {
                [CmdletBinding()]
                param(
                    [parameter(mandatory, position = 0)][int]$counter
                )
                
                #空ファイルの作成
                New-Item C:\File$counter.txt -type file -Force                                 
            }
            # PowerShellオブジェクトの定義。こいつにRunspacePoolとコマンドを定義する。
            $Powershell = [PowerShell]::Create()
            $powershell.RunspacePool = $runspacePool

            #コマンドの定義　AddScriptメソッドでスクリプトブロックまたはコマンドレット単体を挿入する。
            #AddArgumentメソッドではスクリプトブロックのパラメータに引数を渡す。
            #AddCommandではパイプラインの右辺に挿入する。Out-Stringで文字列として結果を出力する
            $powerShell.AddScript($command).AddArgument($i).AddCommand("Out-String")
            #BeginInvokeメソッドで定義したコマンドを実行する。
            $IASyncResult  = $Powershell.BeginInvoke()
             
            $aryPowerShell.add($PowerShell)
            $aryIAsyncResult.add($IASyncResult)
        }
        #個別でRunspacePoolに投げる処理
        #ループ外で個別にPowerShellオブジェクトを生成し、Runspaceに投げてBeginInvokeで違う処理も出来るんだなと勉強したところ。
        Write-Host "sleep 10"
        $PowerShell = [PowerShell]::Create()
        $PowerShell.RunspacePool = $runspacePool
        $PowerShell.AddScript("sleep -Seconds 10").AddCommand("Out-String")
        $IASyncResult = $Powershell.BeginInvoke()

        $aryPowerShell.add($Powershell)
        $aryIAsyncResult.add($IASyncResult)

        
        # 処理結果の確認。$aryPowerShellの中身が空になったら全ての処理が完了したと判断
        while ($aryPowerShell.Count -gt 0){
            for($i =0; $i -lt $aryPowerShell.Count; $i++){
                $PowerShell = $aryPowerShell[$i]
                $IASyncResult = $aryIAsyncResult[$i]

                if($PowerShell -ne $null){
                    #EndInvokeメソッドでは処理結果を受け取る。
                    $result  = $PowerShell.EndInvoke($IASyncResult)
                    Write-Host $result
                    #処理が終了したコマンド(PowerShellオブジェクト)は捨てる
                    $PowerShell.Dispose()
                    #ArrayListから当該オブジェクトを取り除く
                    $aryPowerShell.RemoveAt($i)
                    $aryIAsyncResult.RemoveAt($i)
                    {break outer}
                }
            }
            Start-Sleep -Milliseconds 100
        }
        #全ての処理完了後に作成する空ファイル
        New-Item C:\fileA.txt -type file -Force
    }
    finally{
        # 確実にRunspacePoolを閉じる
        $runspacePool.close()
    }
}

. runspaceTest
```

##ちょっと疑問なところ
まだ解決できてないところがある。
EndInvokeした時点で処理結果を受け取る事が出来るが、今回のようにスクリプトブロックを渡した場合、最後に実行したコマンドの処理結果しか出てこない・・・?
この辺は十分な検証ができてないし私の勘違いかもしれないので後でちゃんとやりたい。


##その他
予めコマンドレットを配列に格納しておきそれぞれで非同期処理を行いたいのであれば、
[RunspacePoolを使って、PowerShellを非同期実行](https://www.gmo.jp/report/single/?art_id=195)
こちらのサンプルコードをコピって配列にコマンドレットを格納し、それを関数に投げるだけでも良いのかと思ったが、**PowerShellを新規に起動してコマンドを実行しているというイメージ**なので例えば**スナップインを追加してから行う**事とか、**vCenterサーバに接続してから行う**事とか、一つのコマンドでやるのは厳しいんじゃね？みたいな事もあったので、スクリプトブロックという手段に行き着いた。

(最初はAddScriptメソッドに仮想マシンのクローンに関するコマンドを投げていたのだが、どうも実行結果を覗いてみると**このコマンドは存在しない**とか**vCenterServerに繋いでない**とか怒られた)


```ps1:スクリプトブロックのところ

            #複数処理実行時用のスクリプトブロック。
            $command = {
                [CmdletBinding()]
                param(
                    [parameter(mandatory, position = 0)][int]$counter
                )

                #空ファイルの作成
                New-Item C:\File$counter.txt -type file -Force                                 
            }


```
paramのあたりで外から変数やオブジェクトを引っ張ってきて、
その下の部分で処理を記述すれば**仮想マシンクローンの作成**とか一度に複数行けそう。

1. パラメータで対象の仮想マシン名などを取得。(xmlとか)
1. vCenterServerに接続
1. 仮想マシンのクローン実行
1. vCenterServerから切断

その辺は職場でサンプルコード仕上げたらまた上げます。


今回は以上。