将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
相关产品推荐
相关产品推荐

