【问题标题】:Wait for multiple simultaneous powershell commands in other sessions to finish before running next commands等待其他会话中的多个同时执行的 powershell 命令完成,然后再运行下一个命令
【发布时间】:2019-10-29 21:49:41
【问题描述】:

我正在尝试获取一个主 powershell 脚本来执行以下操作:

  1. 在其他 3 个 powershell 会话中运行命令(它们都运行大约 1 小时 - 我希望它们同时运行,这样它们所做的工作就可以同时完成)
  2. 等待所有 3 个其他 powershell 会话完成
  3. 在初始 Powershell 窗口中继续执行剩余命令

极其简单的例子

我的实际用例类似于以下,除了时间总是不同,ECHO "hi" 应该只在所有其他 (3) 个命令完成后发生(在这种情况下,我们知道它们需要 10000 秒,但在我的实际用例变化很大)。另请注意,尚不清楚 3 个命令中的哪一个每次花费时间最长。

start powershell { TIMEOUT 2000 }
start powershell { TIMEOUT 3000 }
start powershell { TIMEOUT 10000 }
ECHO "hi"

我可以看到 (here) 我可以在命令前面放置一个&,以便告诉 powershell 等到它完成后再继续执行后续命令。但是,我不知道如何使用 3 个同时命令来做到这一点

【问题讨论】:

  • 你看过powershell作业了吗?它们并行运行,主脚本可以使用各种 *-Job cmdlet 检查结果。
  • @Lee_Dailey 我是新手,从来没有听说过。我会谷歌
  • 这听起来会满足你的需要......祝你好运! [咧嘴]
  • @Lee_Dailey 它看起来更复杂,但听起来就像我所追求的那样。您能否提供一个快速示例,说明问题中示例的代码将如何显示(然后我将尝试针对我的用例进行调整)?此外,在 powershell 终端中看到的输出会发生什么情况,如果 3 个进程同时运行,这是否意味着所有输出都散落在一起? (即在一个窗口中?)
  • 作业在自己的进程中运行。在使用其中一个涉及的 cmdlet 获得结果之前,您什么也得不到。我自己从来没有用过它们——没必要。当我搜索powershell how to use start-job 时,有几篇关于使用这些 cmdlet 的文章。

标签: powershell


【解决方案1】:

您确实在寻找 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)在新窗口中运行命令,默认情况下也是异步,即,串行启动命令确实并行运行

在有限的形式中,这允许您实时监控特定于命令的输出,但它带有以下警告

  • 您必须单独手动激活新窗口才能看到正在生成的输出。

  • 输出仅在命令运行时可见;完成后,它的窗口会自动关闭,因此您无法在事后检查输出。

    • 要解决这个问题,您必须在 PowerShell cmdlet 中使用类似 Tee-Object 的内容,以便同时捕获 文件 中的输出,调用者稍后可以对其进行检查。

    • 这也是使输出以编程方式可用的唯一方法,尽管只能作为文本

  • 通过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

【讨论】:

    【解决方案2】:

    问题的简单答案,使用作业。

    start-job { sleep 2000 }
    start-job { sleep 3000 }
    start-job { sleep 10000 }
    get-job | wait-job
    echo hi
    

    这是另一种方式。 Start 是 start-process 的别名。您可以使用超时而不是睡眠。运行 timeout 3 次实际上看起来很酷,但它可能会弄乱一些输出。

    $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
    

    这是对工作流程的尝试。

    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
    

    或者只是:

    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
    

    使用 3 个运行空间的 API 尝试:

    $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 localhost,localhost,localhost { sleep 5; 'done' }
    
    done
    done
    done
    

    【讨论】:

    • PowerShell 6+ 不支持工作流
    猜你喜欢
    • 1970-01-01
    • 2020-08-08
    • 2019-11-29
    • 2021-10-19
    • 2018-04-03
    • 1970-01-01
    • 2019-12-31
    • 1970-01-01
    • 2011-09-20
    相关资源
    最近更新 更多