PowerShell Runspace异步调用后如何自动清理资源?
PowerShell Runspace自动清理与非阻塞执行实现
我编写了一段PowerShell脚本,在独立Runspace中弹出Windows窗体,并通过计时器在3秒后自动关闭该窗体。需求是打开窗体后脚本能够继续执行后续逻辑,但遇到以下问题:
- 保留
$PowerShell.EndInvoke($AsyncHandle)和$PowerShell.Dispose()时,脚本会被阻塞无法继续执行; - 移除这两行代码,脚本虽能继续执行,但会遗留Runspace句柄。
请问能否在InvocationStateChanged事件的状态判断代码块中,实现Runspace完成时自动清理资源?以下是我的相关代码及尝试:
初始示例代码
$runspace = [runspacefactory]::CreateRunspace() $runspace.ApartmentState = "STA" $runspace.ThreadOptions = "ReuseThread" $runspace.Open() $PowerShell = [PowerShell]::Create().AddScript({ $objForm = New-Object System.Windows.Forms.Form $Timer = New-Object System.Windows.Forms.Timer $Timer.Interval = 1000 $script:countdown = 3 $Timer.Add_Tick({ --$script:countdown if ($script:countdown -lt 0) { $Timer.Dispose(); $objForm.Dispose(); } }) $Timer.Start() $objForm.ShowDialog() | Out-Null }) $PowerShell.Runspace = $runspace $null = Register-ObjectEvent -InputObject $PowerShell -EventName InvocationStateChanged -Action { param([System.Management.Automation.PowerShell] $ps) $state = $EventArgs.InvocationStateInfo.State if ($state -in 'Completed', 'Failed', 'Stopped') { write-host $state $ps.Runspace.Dispose() [GC]::Collect() } } $AsyncHandle = $PowerShell.BeginInvoke() $PowerShell.EndInvoke($AsyncHandle) $PowerShell.Dispose() write-host "Continue with the rest of my script"
尝试1
$runspace = [runspacefactory]::CreateRunspace() $runspace.ApartmentState = "STA" $runspace.ThreadOptions = "ReuseThread" $runspace.Open() $PowerShell = [PowerShell]::Create().AddScript({ $objForm = New-Object System.Windows.Forms.Form $Timer = New-Object System.Windows.Forms.Timer $Timer.Interval = 1000 $script:countdown = 3 $Timer.Add_Tick({ --$script:countdown if ($script:countdown -lt 0) { $Timer.Dispose(); $objForm.Dispose(); } }) $Timer.Start() $objForm.ShowDialog() | Out-Null }) $PowerShell.Runspace = $runspace $null = Register-ObjectEvent -InputObject $PowerShell -EventName InvocationStateChanged -Action { param([System.Management.Automation.PowerShell] $ps) if($EventArgs.InvocationStateInfo.State -in 'Completed', 'Failed', 'Stopped') { $state['Instance'].EndInvoke($state['Handle']) $state['Instance'].Runspace.Dispose() $state['Instance'].Dispose() } } $state = @{ Instance = $PowerShell Handle = $PowerShell.BeginInvoke() } write-host "Continue with the rest of my script" $state['Instance'].Runspace.RunspaceStateInfo.State start-sleep -Seconds 10 $state['Instance'].Runspace.RunspaceStateInfo.State
尝试2
function Show-Form() { $runspace = [runspacefactory]::CreateRunspace() $runspace.ApartmentState = "STA" $runspace.ThreadOptions = "ReuseThread" $runspace.Open() $PowerShell = [PowerShell]::Create().AddScript({ $objForm = New-Object System.Windows.Forms.Form $Timer = New-Object System.Windows.Forms.Timer $Timer.Interval = 1000 $script:countdown = 3 $Timer.Add_Tick({ --$script:countdown if ($script:countdown -lt 0) { $Timer.Dispose(); $objForm.Dispose(); } }) $Timer.Start() $objForm.ShowDialog() | Out-Null }) $PowerShell.Runspace = $runspace $null = Register-ObjectEvent -InputObject $PowerShell -EventName InvocationStateChanged -Action { param([System.Management.Automation.PowerShell] $ps) if($EventArgs.InvocationStateInfo.State -in 'Completed', 'Failed', 'Stopped') { $global:state['Instance'].EndInvoke($global:state['Handle']) $global:state['Instance'].Runspace.Dispose() $global:state['Instance'].Dispose() } } $global:state = @{ Instance = $PowerShell Handle = $PowerShell.BeginInvoke() } } Show-Form #do other stuff do { #write-host $global:state['Instance'].Runspace.RunspaceStateInfo.State start-sleep 1 } while($global:state['Instance'].Runspace.RunspaceStateInfo.State -ne 'closed')
尝试3
function Show-Form() { $runspace = [runspacefactory]::CreateRunspace() $runspace.ApartmentState = "STA" $runspace.ThreadOptions = "UseNewThread" $runspace.Open() $PowerShell = [PowerShell]::Create().AddScript({ $objForm = New-Object System.Windows.Forms.Form $objForm.ShowDialog() | Out-Null }) $PowerShell.Runspace = $runspace $state = @{ Instance = $PowerShell Handle = $PowerShell.BeginInvoke() } $Jobs.Add($state) | Out-Null $null = Register-ObjectEvent -InputObject $state.Instance -MessageData $state.Handle -EventName InvocationStateChanged -Action { param([System.Management.Automation.PowerShell] $ps) if($ps.InvocationStateInfo.State -in 'Completed', 'Failed', 'Stopped') { write-host "closing instance " $ps write-host "closing handle " $Event.MessageData $ps.Runspace.Close() $ps.Runspace.Dispose() $ps.EndInvoke($Event.MessageData) $ps.Dispose() [GC]::Collect() } write-host "finish" } } Show-Form Show-Form write-host "other stuff"
解决方案:在InvocationStateChanged事件中完成资源自动清理
可以通过Register-ObjectEvent的MessageData参数传递异步调用句柄,在事件触发时完成EndInvoke和资源清理,同时避免阻塞主脚本。核心要点:
- 用
MessageData传递BeginInvoke返回的句柄,避免依赖全局变量; - 在事件动作中完成
EndInvoke、Runspace关闭/释放、PowerShell实例释放的完整流程; - 主脚本无需等待
EndInvoke,直接执行后续逻辑。
修改后的完整代码:
function Show-TimedForm { $runspace = [runspacefactory]::CreateRunspace() $runspace.ApartmentState = "STA" $runspace.ThreadOptions = "ReuseThread" $runspace.Open() $psInstance = [PowerShell]::Create().AddScript({ $form = New-Object System.Windows.Forms.Form $form.Text = "自动关闭窗体" $form.Size = [System.Drawing.Size]::new(300, 150) $form.StartPosition = "CenterScreen" $timer = New-Object System.Windows.Forms.Timer $timer.Interval = 1000 $script:countdown = 3 $timer.Add_Tick({ --$script:countdown $form.Text = "剩余 $script:countdown 秒关闭..." if ($script:countdown -lt 0) { $timer.Stop() $timer.Dispose() $form.Close() $form.Dispose() } }) $timer.Start() $form.ShowDialog() | Out-Null }) $psInstance.Runspace = $runspace # 注册InvocationStateChanged事件,传递异步句柄作为MessageData $asyncHandle = $psInstance.BeginInvoke() $null = Register-ObjectEvent -InputObject $psInstance -EventName InvocationStateChanged ` -MessageData $asyncHandle -Action { $state = $EventArgs.InvocationStateInfo.State if ($state -in 'Completed', 'Failed', 'Stopped') { # 完成异步调用并清理资源 $Sender.EndInvoke($Event.MessageData) $Sender.Runspace.Close() $Sender.Runspace.Dispose() $Sender.Dispose() [GC]::Collect() [GC]::WaitForPendingFinalizers() Write-Host "Runspace资源已清理" } } } # 调用函数显示窗体,主脚本不阻塞 Show-TimedForm # 执行后续逻辑 Write-Host "主脚本继续执行..." for ($i=1; $i -le 5; $i++) { Write-Host "主脚本执行步骤 $i" Start-Sleep -Seconds 1 }
关键说明:
MessageData参数安全传递异步句柄,无需全局变量,避免多实例冲突;- 事件动作中按顺序执行
EndInvoke、Runspace关闭、资源释放,确保无句柄残留; - 主脚本调用
Show-TimedForm后立即执行后续代码,完全非阻塞; - 加入
[GC]::WaitForPendingFinalizers()确保垃圾回收完成,彻底释放资源。
内容的提问来源于stack exchange,提问作者Captain_Planet
相关产品推荐
相关产品推荐

