PowerShell 5.1 Runspace中Write-Verbose无输出原因咨询
问题
我需要在PowerShell 5.1中实现多线程脚本,因此采用Runspace(而非ForEach-Object -Parallel)开发。但遇到问题:传递给AddScript方法的脚本块内,Write-Verbose的输出无法显示在脚本输出中,就算用-Verbose参数执行脚本,或者换成Write-Host/Write-Output也不行。
参考代码如下:
[runspacefactory]::CreateRunspacePool() $SessionState = [System.Management.Automation.Runspaces.InitialSessionState]::CreateDefault() $RunspacePool = [runspacefactory]::CreateRunspacePool( 1, #Min Runspaces 5 #Max Runspaces ) $PowerShell = [powershell]::Create() $PowerShell.RunspacePool = $RunspacePool $RunspacePool.Open() $Parameters = @{ Param1 = 'Param1' Param2 = 'Param2' } $jobs = [System.Collections.ArrayList]::New() 1..10 | ForEach { $PowerShell = [powershell]::Create() $PowerShell.RunspacePool = $RunspacePool [void]$PowerShell.AddScript( { Param ( $Param1, $Param2 ) $ThreadID = [appdomain]::GetCurrentThreadId() Write-Verbose "ThreadID: Beginning $ThreadID" -Verbose $Sleep = Get-Random (1..5) [PSCustomObject] @{ Param1 = $Param1 Param2 = $Param2 Thread = $ThreadID ProcessID = $PID SleepTime = $Sleep } Start-Sleep -Seconds $Sleep Write-Verbose "ThreadID: Ending $ThreadID" -Verbose }) [void]$PowerShell.AddParameters($Parameters) $Handle = $PowerShell.BeginInvoke() $temp = '' | Select PowerShell,Handle $temp.PowerShell = $PowerShell $temp.handle = $Handle [void]$jobs.Add($Temp) Write-Host ("Available Runspaces in RunspacePool: {0}" -f $RunspacePool.GetAvailableRunspaces()) Write-Host ("Remaining Jobs: {0}" -f @($jobs | Where { $_.handle.iscompleted -ne 'Completed' }).Count) } # Verify completed Write-Host ("Available Runspaces in RunspacePool: {0}" -f $RunspacePool.GetAvailableRunspaces()) Write-Host ("Remaining Jobs: {0}" -f @($jobs | Where { $_.handle.iscompleted -ne 'Completed' }).Count) Write-Verbose "Waiting for jobs to finish..." -Verbose $return = $jobs | ForEach { $_.powershell.EndInvoke($_.handle) $_.PowerShell.Dispose() } Write-Verbose "All jobs have finished" -Verbose $jobs.Clear() $return | Group ProcessID | Select Count, Name $return | Group Thread | Select Count, Name ($return | Group Thread).Count
执行脚本后的输出示例:
C:\> .\parallel.ps1 InstanceId : a965701a-f4d2-45d2-8200-f75fe77d6f72 IsDisposed : False RunspacePoolStateInfo : System.Management.Automation.RunspacePoolStateInfo InitialSessionState : ConnectionInfo : CleanupInterval : 00:15:00 RunspacePoolAvailability : None ThreadOptions : Default ApartmentState : Unknown Available Runspaces in RunspacePool: 4 Remaining Jobs: 1 Available Runspaces in RunspacePool: 3 Remaining Jobs: 2 Available Runspaces in RunspacePool: 2 Remaining Jobs: 3 Available Runspaces in RunspacePool: 1 Remaining Jobs: 4 Available Runspaces in RunspacePool: 0 Remaining Jobs: 5 Available Runspaces in RunspacePool: 0 Remaining Jobs: 6 Available Runspaces in RunspacePool: 0 Remaining Jobs: 7 Available Runspaces in RunspacePool: 0 Remaining Jobs: 8 Available Runspaces in RunspacePool: 0 Remaining Jobs: 9 Available Runspaces in RunspacePool: 0 Remaining Jobs: 10 Available Runspaces in RunspacePool: 0 Remaining Jobs: 10 VERBOSE: Waiting for jobs to finish... VERBOSE: All jobs have finished Count : 10 Name : 5400 Count : 2 Name : 14208 Count : 3 Name : 7708 Count : 2 Name : 13584 Count : 1 Name : 1940 Count : 2 Name : 17756 5
原因与解决方法
核心原因
Runspace中的脚本块运行在独立的执行上下文里,默认不会将流输出(Verbose、Host、Output等)转发到主控制台。另外:
Write-Verbose依赖当前执行上下文的VerbosePreference设置,Runspace默认不会继承主会话的偏好设置;BeginInvoke()/EndInvoke()仅捕获脚本块的返回值(即示例中的[PSCustomObject]部分),不会捕获其他流的输出。
解决方法
要捕获并显示Runspace内的输出,需要为每个[powershell]实例设置输出流的重定向,同时继承主会话的偏好设置。
修改后的代码
# 初始化Runspace池 $RunspacePool = [runspacefactory]::CreateRunspacePool(1, 5) $RunspacePool.Open() $Parameters = @{ Param1 = 'Param1' Param2 = 'Param2' } $jobs = [System.Collections.ArrayList]::New() 1..10 | ForEach { $psInstance = [powershell]::Create() $psInstance.RunspacePool = $RunspacePool # 传递主会话的Verbose状态 $psInstance.AddParameter('Verbose', ($VerbosePreference -eq 'Continue')) | Out-Null [void]$psInstance.AddScript( { Param ( $Param1, $Param2, [switch]$Verbose ) # 显式设置当前上下文的Verbose偏好 if ($Verbose) { $VerbosePreference = 'Continue' } $ThreadID = [appdomain]::GetCurrentThreadId() Write-Verbose "ThreadID: Beginning $ThreadID" $Sleep = Get-Random (1..5) $result = [PSCustomObject] @{ Param1 = $Param1 Param2 = $Param2 Thread = $ThreadID ProcessID = $PID SleepTime = $Sleep } Start-Sleep -Seconds $Sleep Write-Verbose "ThreadID: Ending $ThreadID" return $result }) [void]$psInstance.AddParameters($Parameters) # 监听Verbose流,实时输出到控制台 $psInstance.Streams.Verbose.DataAdded += { param($sender, $e) $entry = $sender[$e.Index] Write-Host "VERBOSE: $($entry.Message)" } $Handle = $psInstance.BeginInvoke() $temp = '' | Select-Object PowerShell, Handle $temp.PowerShell = $psInstance $temp.Handle = $Handle [void]$jobs.Add($Temp) Write-Host ("Available Runspaces in RunspacePool: {0}" -f $RunspacePool.GetAvailableRunspaces()) Write-Host ("Remaining Jobs: {0}" -f @($jobs | Where-Object { !$_.Handle.IsCompleted }).Count) } # 等待所有任务完成并处理结果 Write-Verbose "Waiting for jobs to finish..." -Verbose $return = $jobs | ForEach-Object { $result = $_.PowerShell.EndInvoke($_.Handle) $_.PowerShell.Dispose() $result } Write-Verbose "All jobs have finished" -Verbose $jobs.Clear() $return | Group-Object ProcessID | Select-Object Count, Name $return | Group-Object Thread | Select-Object Count, Name ($return | Group-Object Thread).Count
关键修改点
- 继承Verbose偏好:将主会话的
VerbosePreference状态传递给Runspace内的脚本块,显式设置上下文的输出偏好; - 实时捕获流输出:通过
$psInstance.Streams.Verbose.DataAdded事件监听Verbose流,一旦有输出就实时转发到主控制台; - 分离实例变量:将循环内的
$PowerShell变量改为$psInstance,避免变量覆盖导致的潜在问题。
内容的提问来源于stack exchange,提问作者James Bostock
相关产品推荐
相关产品推荐

