You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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和资源清理,同时避免阻塞主脚本。核心要点:

  1. 用MessageData传递BeginInvoke返回的句柄,避免依赖全局变量;
  2. 在事件动作中完成EndInvoke、Runspace关闭/释放、PowerShell实例释放的完整流程;
  3. 主脚本无需等待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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.28 11:27:02