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

将PowerShell类实例传递至工作线程调用嵌套方法的异常问题

PowerShell自定义类实例跨Runspace/ThreadJob传递导致任务挂起问题

当通过$using:或-ArgumentList向ThreadJobs或Runspaces传递PowerShell自定义类实例时,会出现任务始终处于“Running”状态、无法正常完成的异常。以下是可复现问题的代码:

$ErrorActionPreference = "Stop"
Set-StrictMode -Version Latest

class test_class : System.IDisposable
{
    test_class([int] $Id)
    {
        $this._id = $Id
    }

    [int] $_id

    [void] Dingaling()
    {
        "{0}:{1} is disposed? {2}" -f $this.GetType(), $this._id, $this._disposed | Out-Host
    }

    [int] GetId()
    {
        return $this._id
    }

    [void] NullFunc()
    {
        # does nothing at all!
    }

    [void] CallThisNullFunc()
    {
        "calling this.NullFunc" | Out-Host
        "our type is $($this.GetType())" | Out-Host
        $this | Out-String | Out-Host
        #$this.NullFunc | Out-String | Out-Host # fatal
        $this.NullFunc() # fatal
        "returned from this.NullFunc" | Out-Host
    }

    [void] CallThisDispose()
    {
        "yo ho ho, callin' this.Dispo'" | Out-Host
        $this.Dispose()
    }
    
    $_disposed = $false

    [void] Dispose()
    {
        $this.Dispose($true)
        #[System.GC]::SuppressFinalize($this)
    }

    [void] Dispose([bool] $Disposing)
    {
        "yarrr we be disposin' {0}!" -f $this._id | Out-Host
        $this._disposed = $true
    }
    
    [void] Finalize()
    {
        "yarrr we be finalizin' {0}!" -f $this._id | Out-Host
        $this.Dispose($false)
    }
} # end class

function test_func2
{
    $test2 = [test_class]::new(2)
    $test2.Dingaling()
    #$test2 = $null # should be collected by gc
}

test_func2
"call gc after test_func2"
[System.GC]::Collect()

$otherPs = [powershell]::Create()
$otherPs.RunspacePool = [runspacefactory]::CreateRunspacePool(1,2)
$otherPs.RunspacePool.Open()

function test_func
{
    # use a function so test goes out of scope
    $test = [test_class]::new(1)

    $otherPs.AddScript({param($test)
        #$test.Dispose() # no problems if called here
        $test.CallThisNullFunc() # fine here
        $test.Dingaling()
        
        Start-Sleep -Seconds 10
        
        $test.Dingaling()
        $test.Dingaling()
        #$test.Dispose() # fatal if called here
        $test.Dingaling()
        "our id is $($test.GetId())"
        $test.NullFunc() # this is fine
        $test.CallThisNullFunc() # fatal if called here, script will never end
        #$test.CallThisDispose() # also fatal here
        #$test.Dispose() # or here
    })
    $otherPs.AddArgument($test)
}

"call test_func"
test_func

"begin invoke"
$timer = [System.Diagnostics.Stopwatch]::new()
$timer.Start()
$runState = $otherPs.BeginInvoke()

#Start-Sleep -Seconds 15 # doesn't help

while (-not $runState.IsCompleted) {
    "not done yet: {0}" -f ($runState | Out-String)
    Start-Sleep -MilliSeconds 3000
    [System.GC]::Collect()

    if (20 -lt $timer.Elapsed.TotalSeconds) {
        "terminate test after {0}s" -f $timer.Elapsed.TotalSeconds
        break
    }
}

$otherPs.InvocationStateInfo # "running"... forever
$otherPs.EndInvoke($runState) # fails to return
"runspace faulted? {0}" -f $otherPs.HadErrors
$otherPs.Streams.Error | Out-Host

$otherPs.RunspacePool.Close()
$otherPs.Dispose()
$otherPs = $null

1..10 | ForEach-Object {
    Start-Sleep -MilliSeconds 1000
    "do gc collection then quit"
    [System.GC]::Collect()
}

实际场景痛点

  • 原本期望通过.NET异步方法的延续任务释放资源:$httpResponse.Content.Stream.CopyAsync($outStream).ContinueWith({$outstream.Dispose(); $httpResponse.Dispose()}),但由于任务线程没有PowerShell默认运行空间,该方案不可行。
  • 临时采用的方案是将IDisposable对象保存为类字段,但这种方式不够优雅,且无法保证对象被释放——如果类的使用者未调用$myClass.Cleanup(),这些字段可能会阻碍GC回收对象。
  • 此外,将$httpResponse传递至ThreadJob或新Runspace时也会触发上述任务挂起的异常,且无法在Job脚本内部创建$httpResponse,因为该对象由代码的其他部分生成。

PowerShell的线程处理存在一些局限性,使用时需要额外注意这类跨线程的对象传递问题。

内容的提问来源于stack exchange,提问作者Cainan Barbie

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.21 17:06:29