在运行下一个命令之前,请等待其他会话中同时运行的多个PowerShell命令完成。
在运行下一个命令之前,请等待其他会话中同时运行的多个PowerShell命令完成。
我正在尝试编写一个主要的PowerShell脚本,以实现以下功能:
- 在3个其他PowerShell会话中运行命令(它们大约需要1小时-我希望它们同时运行,以便可以同时完成它们的工作)
- 等待所有3个其他PowerShell会话完成
- 继续执行初始PowerShell窗口中的剩余命令
极简示例
我的实际用例类似于以下示例,不过时间总是不同的,只有在其他(3个)命令全部完成后(在这种情况下我们知道它们将花费10000秒,但在我的实际用例中时间变化很大)才会执行ECHO "hi"
。同时需要注意的是,每次都不清楚哪个命令会花费最长的时间。
start powershell { TIMEOUT 2000 } start powershell { TIMEOUT 3000 } start powershell { TIMEOUT 10000 } ECHO "hi"
我可以在命令前面加上&
(参见这里)以告诉PowerShell在继续执行后续命令之前等待其完成。然而,我不知道如何同时处理3个命令。
在使用PowerShell时,有时候需要等待多个同时运行的命令完成后再执行下一个命令。下面是解决这个问题的原因和方法:
原因:
- 使用`Start-Job`创建后台作业时,每个作业都在自己的进程中运行,这会引入很大的开销。
- 使用`Start-Job`创建的作业在运行过程中可能会导致类型不一致的问题。
解决方法:
- 使用轻量级的`ThreadJob`模块来代替`Start-Job`,这个模块基于线程实现。
- 在PowerShell [Core] v6+中自带了`ThreadJob`模块,在Windows PowerShell中可以使用`Install-Module ThreadJob -Scope CurrentUser`命令进行安装。
- 通过调用`Start-ThreadJob`来启动线程作业,使用标准的`*-Job`命令来管理这些作业。
- 可以使用`Receive-Job -Wait -AutoRemoveJob`命令等待所有作业完成,并自动清理作业。
- 通过`Start-ThreadJob`创建的作业的输出会在可用时立即显示,但是脚本的执行会等到所有作业完成后才继续。
下面是一个示例代码:
$startedAt = [datetime]::UtcNow $commands = { $n = 2; Start-Sleep $n; "I ran for $n secs." }, { $n = 3; Start-Sleep $n; "I ran for $n secs." }, { $n = 10; Start-Sleep $n; "I ran for $n secs." } $jobs = $commands | Foreach-Object { Start-ThreadJob $_ } $jobs | Receive-Job -Wait -AutoRemoveJob "All jobs completed. Total runtime in secs.: $(([datetime]::UtcNow - $startedAt).TotalSeconds)"
使用`Start-ThreadJob`创建的作业输出如下:
I ran for 2 secs. I ran for 3 secs. I ran for 10 secs. All jobs completed. Total runtime in secs.: 10.2504931
如果想要按照每个命令的输出显示结果,可以使用以下代码:
$startedAt = [datetime]::UtcNow $commands = { $n = 1; Start-Sleep $n; "I ran for $n secs." }, { $n = 2; Start-Sleep $n; "I ran for $n secs." }, { $n = 3; Start-Sleep $n; "I ran for $n secs." } $jobs = $commands | Foreach-Object { Start-ThreadJob $_ } $null = Wait-Job $jobs foreach ($job in $jobs) { "`n--- Output from {$($job.Command)}:" Receive-Job $job } "`nAll jobs completed. Total runtime in secs.: $('{0:N2}' -f ([datetime]::UtcNow - $startedAt).TotalSeconds)"
输出结果如下:
--- Output from { $n = 1; Start-Sleep $n; "I ran for $n secs." }: I ran for 1 secs. --- Output from { $n = 2; Start-Sleep $n; "I ran for $n secs." }: I ran for 2 secs. --- Output from { $n = 3; Start-Sleep $n; "I ran for $n secs." }: I ran for 3 secs. All jobs completed. Total runtime in secs.: 3.09
另外,可以使用`Start-Process`命令在新窗口中运行命令,实现异步执行。但是需要注意的是,这种方法需要手动激活新窗口来查看输出,而且命令执行完成后窗口会自动关闭,无法在命令执行完成后查看输出。使用`Start-Process`命令还会引入较大的处理开销。
等待其他会话中多个同时运行的PowerShell命令完成后再运行下一个命令的问题可能出现的原因是需要等待多个后台任务完成才能继续执行后续命令。
解决方法有多种,下面将对每种方法进行整理:
方法一:使用jobs命令
start-job { sleep 2000 } start-job { sleep 3000 } start-job { sleep 10000 } get-job | wait-job echo hi
方法二:使用start命令和timeout命令
$a = start -NoNewWindow powershell {timeout 10; 'a done'} -PassThru $b = start -NoNewWindow powershell {timeout 10; 'b done'} -PassThru $c = start -NoNewWindow powershell {timeout 10; 'c done'} -PassThru $a,$b,$c | wait-process 'hi' b done c done a done hi
方法三:使用workflow
function sleepfor($time) { sleep $time; "sleepfor $time done"} workflow work { parallel { sleepfor 3 sleepfor 2 sleepfor 1 } 'hi' } work sleepfor 1 done sleepfor 2 done sleepfor 3 done hi
方法四:使用foreach-parallel循环
function sleepfor($time) { sleep $time; "sleepfor $time done"} workflow work2 { foreach -parallel ($i in 1..3) { sleepfor 10 } 'hi' } work2 # runs in about 13 seconds sleepfor 10 done sleepfor 10 done sleepfor 10 done hi
方法五:使用Api和3个runspaces
$a = [PowerShell]::Create().AddScript{sleep 5;'a done'} $b = [PowerShell]::Create().AddScript{sleep 5;'b done'} $c = [PowerShell]::Create().AddScript{sleep 5;'c done'} $r1,$r2,$r3 = ($a,$b,$c).begininvoke() $a.EndInvoke($r1); $b.EndInvoke($r2); $c.EndInvoke($r3) ($a,$b,$c).dispose() a done b done c done
方法六:使用远程invoke-command命令(需要提升权限)
invoke-command localhost,localhost,localhost { sleep 5; 'done' } done done done
需要注意的是,PowerShell 6+版本不再支持workflow方法。