Powershellで非同期実行
Start-Jobで非同期実行っぽいことができるわけですが、あれは別プロセス。
しかも、Receive-Jobしないと結果取得できない。
スレッドでやりたいじゃない?
そこを解決してみる。
追記 2014/12/03
Start-Timerのスクリプトブロックに引数を渡せるようにした
スクリプトブロック内では、$Event.MessageDataで参照可能
追記 2014/12/17
Start-Taskに
終了時実行するスクリプトブロックを渡せるようにした
STAで実行できるようにスイッチをつけた(PowerShell v3以降だと初期STAなのにStart-TaskはMTAになるため)
(そうかー、v3以降は初期STAかぁ、、、)
調べる
なんか、Runspaceとかいうの使ったりPowershellクラス使ったりするらしい。
面倒。
あれ、Timerはどうだろう?
ということで、Timerで作る。
Timer実行スクリプト
Timerのイベントもらうには、Register-ObjectEventしないといけないらしい。
名前を省略すると自動生成するようにしてみた。
function Start-Timer() { Param( $ScriptBlock, [int]$Interval = 0, [string]$Name = '', $MessageData = $null ) if([string]::IsNullOrEmpty($Name)) { $Name = 'JunjunTimer' $id = 1 $jid = 0 foreach($j in Get-Job) { if($j.Name.StartsWith($Name) -and [int]::TryParse($j.Name.Substring($Name.Length), [ref]$jid) -and $id -le $jid) { $id = $jid + 1 } } $Name += $id } $timer = New-Object System.Timers.Timer if($Interval -le 0) { $Interval = 1 $timer.AutoReset = $false } $timer.Interval = $Interval $ret = Register-ObjectEvent -InputObject $timer -EventName Elapsed -SourceIdentifier $Name -Action $ScriptBlock -MessageData $MessageData $timer.Enabled = $true return $ret } function Remove-Timer() { Param( [string]$Name = '' ) if([string]::IsNullOrEmpty($Name)) { $n = 'JunjunTimer' $id = 0 $jid = 0 foreach($j in Get-Job) { if($j.Name.StartsWith($n) -and [int]::TryParse($j.Name.Substring($n.Length), [ref]$jid) -and ($id -eq 0 -or $id -gt $jid)) { $Name = $j.Name $id = $jid } } } Unregister-Event $Name Remove-Job $Name }
使ってみる。
Start-Timer { Write-Host 1 ([DateTime]::Now) } 2000 Start-Timer { Write-Host 2 ([DateTime]::Now) } 3000
2秒間隔および3秒間隔で時刻が表示される!
これでいけそう!
止める。
Remove-Timer Remove-Timer
じゃ、これで完成。
んだば、重い処理流してみるか、、、その前にテスト。
Start-Timer { Write-Host 1 ([DateTime]::Now); Start-Sleep 10; Write-Host 1 ([DateTime]::Now) } Start-Timer { Write-Host 2 ([DateTime]::Now); Start-Sleep 10; Write-Host 2 ([DateTime]::Now) }
、、、アカン。
1が終わってからじゃないと2が動かない。
入力も受け付けない。
これメインにInvokeしてる。(多分)
仕方ない、Runspace使うか。
Runspace実行スクリプト
RunspacePool作って、Powershellオブジェクト作ってBeginInvoke。
終わったらEndInvokeして、Disposeする、と。
function Start-Task() { Param( $ScriptBlock ) $rspool = [RunspaceFactory]::CreateRunspacePool() $rspool.Open() $psh = [PowerShell]::Create() $psh.RunspacePool = $rspool $psh.AddScript($ScriptBlock) | Out-Null $ret = New-Object PSObject -Property @{ rspool = $rspool psh = $psh asyncResult = $psh.BeginInvoke() } return $ret } function Wait-Task() { Param( $Task ) if($Task -ne $null -and $Task.asyncResult -ne $null -and $Task.asyncResult.IsCompleted) { $ret = $Task.psh.EndInvoke($Task.asyncResult) $Task.psh.Dispose() $Task.rspool.Close() } else { $ret = 'Running' } return $ret }
なんだ、やってみれば簡単ね。
ええと、Start-TaskしたオブジェクトでWait-Taskに問い合わせるのだから、、、
$job = Start-Task { Start-Sleep 10 Write-Host 1 ([DateTime]::Now) } Wait-Task $job
おお「Running」って。
終わったころに「Wait-Task $job」すると、、、あれ、何も返ってこない。
あ、returnじゃないとダメか。
$job = Start-Task { Start-Sleep 10 1;([DateTime]::Now) } Wait-Task $job
一応できた。
んー、、、Wait-Taskしないと結果取れない。
終わり次第にわかるわけじゃない、、、。
じゃあ、MessageBox出せば良いじゃない?
Add-Type -AssemblyName System.Windows.Forms $job = Start-Task { Start-Sleep 10 [System.Windows.Forms.MessageBox]::Show([DateTime]::Now.ToString(), 1) } Wait-Task $job
、、、PowershellがActiveじゃないと気づかない。
ってか、結果表示MessageBoxを書かないといけないのが面倒。
だいたいAdd-Typeしたくないこともあるかもしれない。
Wait-Taskなんてしたくない。
でも、、、他に方法が、、、じゃあ、これで、、、。
いやいやいや!
そういえば、Write-Hostできる人がいたねぇ。
それ組み合わせようよ。
完成スクリプト
Timer実行の関数は上のと同じなのでそれ使ってね。
function Start-Task() { Param( [scriptblock]$ScriptBlock, [string]$Name = $null, [switch]$Silent, $MessageData = $null, [scriptblock]$EndScript = $null, [switch]$Sta ) $wn = 'JunjunTaskWatcher' foreach($j in Get-Job) { if($j.Name -eq $wn) { $wn = $null break } } if(![string]::IsNullOrEmpty($wn)) { Start-Timer { foreach($e in Get-Event) { if($e.SourceIdentifier -eq 'JunjunTaskEvent') { if($e.MessageData -ne $null -and $e.MessageData.asyncResult -ne $null) { if(![string]::IsNullOrEmpty($e.MessageData.out)) { Write-Host $e.MessageData.name ':' $e.MessageData.out $e.MessageData.out = $null } if($e.MessageData.asyncResult.IsCompleted) { $e.MessageData.result = $e.MessageData.psh.EndInvoke($e.MessageData.asyncResult) if(!$e.MessageData.silent) { Write-Host 'Finish!' $e.MessageData.name ':' $e.MessageData.result } if($e.MessageData.end -ne $null) { &($e.MessageData.end)($e.MessageData) } $e.MessageData.psh.Dispose() $e.MessageData.rspool.Close() Remove-Event -EventIdentifier $e.EventIdentifier } } else { Remove-Event -EventIdentifier $e.EventIdentifier } } } } 500 $wn | Out-Null } $rspool = [RunspaceFactory]::CreateRunspacePool() if($Sta) { $rspool.ApartmentState = 'STA' } $rspool.Open() $psh = [PowerShell]::Create() $psh.RunspacePool = $rspool $ret = New-Object PSObject -Property @{ name = $Name rspool = $rspool psh = $psh asyncResult = $null out = $null result = $null silent = $Silent end = $EndScript } $psh.AddScript($ScriptBlock).AddArgument($ret).AddArgument($MessageData) | Out-Null $ret.asyncResult = $psh.BeginInvoke() New-Event JunjunTaskEvent -MessageData $ret | Out-Null return $ret }
解説他
例えば、uwscプロセスの終了監視なら以下のような感じ。
Start-Task { $args[0].out += 'start' $p = Get-Process uwsc while($p -ne $null -and !$p.HasExited) { Start-Sleep 1 } } 'uwsc watcher'
$args[0].outに文字列を入れると、JunjunTaskWatcherタイマーがWrite-Hostします。
また終了すると結果もWrite-Hostします。
JunjunTaskWatcherタイマーがなければ、500msec間隔起動で作成します。
実行結果を受け取りたい場合は、
$job = Start-Task { $args[0].out += 'start' $p = Get-Process uwsc while($p -ne $null -and !$p.HasExited) { Start-Sleep 1 } $p } 'uwsc watcher'
といった形で変数に受け取り、
$job.result
を見てください。
あと、これ書いてる過程で先日のNotifyIconのProcess.Exitedイベントの受け取り方がわかった。
ので修正してみた。
UWSCでタスクトレイ常駐する - じゅんじゅんのきまぐれ