Powershellで非同期実行

Start-Jobで非同期実行っぽいことができるわけですが、あれは別プロセス。
しかも、Receive-Jobしないと結果取得できない。
スレッドでやりたいじゃない?
そこを解決してみる。

追記 2014/12/03
Start-Timerのスクリプトブロックに引数を渡せるようにした
スクリプトブロック内では、$Event.MessageDataで参照可能


Start-Taskのスクリプトブロックに引数を渡せるようにした
スクリプトブロック内では、$args[1]で参照可能

追記 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でタスクトレイ常駐する - じゅんじゅんのきまぐれ