您确实在寻找 Powershell background jobs,正如 Lee Daily 所建议的那样。
但是,作业是繁重的,因为每个作业都在自己的进程中运行,这会带来显着的开销,并且也可能导致类型保真度丢失(由于涉及 PowerShell 的基于 XML 的序列化基础架构 - 请参阅 this answer)。
ThreadJob module 提供了一种基于线程 的轻量级替代方案。
它带有 PowerShell [Core] v6+,并且可以在 Windows PowerShell 中按需安装,例如,
Install-Module ThreadJob -Scope CurrentUser。[1]
您只需调用Start-ThreadJob 而不是Start-Job,并使用标准的*-Job cmdlet 来管理此类线程作业 - 就像管理常规后台作业一样。 p>
这是一个例子:
$startedAt = [datetime]::UtcNow
# Define the commands to run as [thread] jobs.
$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." }
# Start the (thread) jobs.
# You could use `Start-Job` here, but that would be more resource-intensive
# and make the script run considerably longer.
$jobs = $commands | Foreach-Object { Start-ThreadJob $_ }
# Wait until all jobs have completed, passing their output through as it
# is received, and automatically clean up afterwards.
$jobs | Receive-Job -Wait -AutoRemoveJob
"All jobs completed. Total runtime in secs.: $(([datetime]::UtcNow - $startedAt).TotalSeconds)"
以上产生如下内容;请注意,单个命令的输出会在可用时报告,但调用脚本的执行在所有命令完成之前不会继续:
I ran for 2 secs.
I ran for 3 secs.
I ran for 10 secs.
All jobs completed. Total runtime in secs.: 10.2504931
注意:在这个简单的例子中,哪个输出来自哪个命令是很明显的,但更典型的是,各种作业的输出会不可预测地交错运行,这使得它难以解释输出 - 请参阅下一节以获取解决方案。
如您所见,在后台基于线程的并行执行引入的开销是最小的 - 整体执行只花费了 10 秒多一点,这是 3 个命令中运行时间最长的一个。
如果您改用基于进程的Start-Job,则总体执行时间可能如下所示,显示引入的大量开销,尤其是在您第一次在会话中运行后台作业时:
All jobs completed. Total runtime in secs.: 18.7502717
也就是说,至少在会话中的第一次调用中,后台并行执行的好处被否定了 - 执行所花费的时间比这种情况下顺序执行所花费的时间要长。
虽然同一会话中后续基于进程的后台作业运行得更快,但开销仍然明显高于基于线程的作业。
同步作业输出流
如果你想显示后台命令的输出每个命令,你需要单独收集输出。
注意:在控制台窗口(终端)中,这要求您等到所有命令完成后才能显示输出(因为无法通过就地更新同时显示多个输出流,至少使用常规输出命令)。
$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 $_ }
# Wait until all jobs have completed.
$null = Wait-Job $jobs
# Collect the output individually for each job and print it.
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 在单独的窗口中运行命令
在Windows上,您可以使用Start-Process(别名为start)在新窗口中运行命令,默认情况下也是异步,即,串行启动命令确实并行运行。
在有限的形式中,这允许您实时监控特定于命令的输出,但它带有以下警告:
-
您必须单独手动激活新窗口才能看到正在生成的输出。
-
输出仅在命令运行时可见;完成后,它的窗口会自动关闭,因此您无法在事后检查输出。
-
通过Start-Process 将PowerShell 命令传递给powershell.exe 需要您将命令作为字符串(而不是脚本块)传递,并且有烦人的解析要求,例如需要转义"字符。 \" (原文如此) - 见下文。
-
最后同样重要的是,使用 Start-Process 还会引入显着的处理开销(尽管运行时间很长的命令可能无关紧要)。
$startedAt = [datetime]::UtcNow
# Define the commands - of necessity - as *strings*.
# Note the unexpected need to escape the embedded " chars. as \"
$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.\"'
# Use `Start-Process` to launch the commands asynchronously,
# in a new window each (Windows only).
# `-PassThru` passes an object representing the newly created process through.
$procs = $commands | ForEach-Object { Start-Process -PassThru powershell -Args '-c', $_ }
# Wait for all processes to exit.
$procs.WaitForExit()
"`nAll processes completed. Total runtime in secs.: $('{0:N2}' -f ([datetime]::UtcNow - $startedAt).TotalSeconds)"
[1] 在 Windows PowerShell v3 和 v4 中,Install-Module 默认不可用,因为这些版本不附带 PowerShellGet 模块。不过这个模块可以按需安装,详见Installing PowerShellGet