繁体   English   中英

无法使用 foreach-object 并行启动作业

[英]Cannot start job with foreach-object in parallel

我准备了这个脚本来尝试使用不同的参数多次并行执行相同的函数:

$myparams = "A", "B","C", "D"

$doPlan = {
    Param([string] $myparam)
        echo "print $myparam"
        # MakeARestCall is a function calling a web service
        MakeARestCall -myparam $myparam
        echo "done"
}

$myparams | Foreach-Object { 
    Start-Job -ScriptBlock $doPlan  -ArgumentList $_
}

当我运行它时,输出是

Id     Name            PSJobTypeName   State         HasMoreData     Location             Command                  
--     ----            -------------   -----         -----------     --------             -------                  
79     Job79           BackgroundJob   Running       True            localhost            ...                      
81     Job81           BackgroundJob   Running       True            localhost            ...                      
83     Job83           BackgroundJob   Running       True            localhost            ...                      
85     Job85           BackgroundJob   Running       True            localhost            ...

但对块(然后是 Web 服务)的实际调用并未完成。 如果我删除 foreach-object 并将其替换为没有 Start-Job 的正常顺序 foreach 块,则可以正确调用 web 服务。 这意味着当我尝试并行运行块时我的问题。

我究竟做错了什么?

后台作业在独立的子进程中运行,与调用者几乎不共享任何状态 具体来说:

  • 他们看不到调用会话中定义的任何函数和别名,也看不到手动导入的模块,也看不到手动加载的 .NET 程序集。

  • 他们不会加载(点源)您的$PROFILE文件,因此他们不会从那里看到任何定义。

  • 在 PowerShell 6.x 及以下版本(包括 Windows PowerShell)中,甚至当前位置(目录)都不是从调用者继承的(默认为[Environment]::GetFolderPath('MyDocuments') ); 这是在 v7.0 中修复的。

  • 他们看到的调用会话状态的唯一方面是调用进程的环境变量的副本。

  • 要使调用者会话中的变量值可用于后台作业,必须通过$using:scope引用它们(请参阅about_Remote_Variables )。

    • 请注意,对于字符串、原始类型(例如数字)和少数其他知名类型以外的值,这可能会导致类型保真度的损失,因为这些值使用 PowerShell 的基于 XML 的序列化和反序列化; 这种潜在的类型保真度损失也会影响作业的输出- 有关背景信息,请参阅此答案
    • 通过Start-ThreadJob使用速度更快、资源占用更少的线程作业可以避免这个问题(尽管所有其他限制都适用); Start-ThreadJob随 PowerShell [Core] 6+ 一起提供,并且可以在 Windows PowerShell 中按需安装(例如, Install-Module -Scope CurrentUser ThreadJob ) - 有关背景信息,请参阅此答案

重要提示每当您将作业用于自动化时,例如在从 Windows 任务计划程序调用的脚本中或在 CI/CD 的上下文中,请确保在退出脚本之前等待所有作业完成(通过Receive-Job -WaitWait-Job ),因为通过 PowerShell 的CLI调用的脚本会作为一个整体退出 PowerShell 进程,从而终止所有未完成的作业。

因此,除非命令MakeARestCall

  • 恰好是位于$env:Path列出的目录之一中的脚本文件MakeARestCall.ps1 )或可执行文件MakeARestCall.exe

  • 恰好是在自动加载模块中定义的函数,

您的$doJob脚本块在作业进程中执行时将失败,因为既不会定义MakeARestCall函数也不会定义别名。

您的评论表明MakeARestCall确实是一个function ,因此为了使您的代码正常工作,您必须(重新)将该函数定义为作业执行的脚本块的一部分(在您的情况下$doJob ):

以下简化示例演示了该技术:

# Sample function that simply echoes its argument.
function MakeARestCall { param($MyParam) "MakeARestCall: $MyParam" }

'foo', 'bar' | ForEach-Object {
  # Note: If Start-ThreadJob is available, use it instead of Start-Job,
  #       for much better performance and resource efficiency.
  Start-Job -ArgumentList $_ { 

    Param([string] $myparam)

    # Redefine the function via its definition in the caller's scope.
    # $function:MakeARestCall returns MakeARestCall's function body
    # which $using: retrieves from the caller's scope, assigning to
    # it defines the function in the job's scope.
    $function:MakeARestCall = $using:function:MakeARestCall

    # Call the recreated MakeARestCall function with the parameter.
    MakeARestCall -MyParam $myparam
  }
} | Receive-Job -Wait -AutoRemove

上面的输出MakeARestCall: fooMakeARestCall: bar ,表明在作业的过程中成功调用了(重新定义的) MakeARestCall函数。

一种方法

MakeARestCall脚本MakeARestCall.ps1 )和呼叫通过其完整路径,是安全的。

例如,如果您的脚本与调用脚本位于同一文件夹中,则将其调用
& $using:PSScriptRoot\\MakeARestCall.ps1 -MyParam $myParam

当然,如果您不介意复制函数定义或在后台作业的上下文中需要它,您可以简单地将函数定义直接嵌入到脚本块中。


更简单、更快的 PowerShell [Core] 7+ 替代方案,使用ForEach-Object -Parallel

PowerShell 7 中引入ForEach-Object-Parallel参数为每个管道输入对象在单独的运行空间(线程)中运行给定的脚本块。

本质上,它是使用线程作业( Start-ThreadJob ) 的一种更简单、管道友好的方式,与后台作业相比具有相同的性能和资源使用优势,并增加了直接报告线程输出的简单性

但是,对于上述讨论到后台作业缺乏的状态共享适用螺纹工作(即使它们运行在相同的过程中,他们在孤立的PowerShell运行空间这样做),所以这里也MakARestCall必须(重新)定义的函数(或嵌入)在脚本块[1] 内

# Sample function that simply echoes its argument.
function MakeARestCall { param($MyParam) "MakeARestCall: $MyParam" }

# Get the function definition (body) *as a string*.
# This is necessary, because the ForEach-Object -Parallel explicitly
# disallows referencing *script block* values via $using:
$funcDef = $function:MakeARestCall.ToString()

'foo', 'bar' | ForEach-Object -Parallel {
  $function:MakeARestCall = $using:funcDef
  MakeARestCall -MyParam $_
}

语法陷阱: -Parallel不是开关(标志类型参数),而是将并行运行的脚本块作为参数; 换句话说: -Parallel必须直接放置在脚本块之前。

以上直接从并行线程发出输出,因为它到达 - 但请注意,这意味着输出不能保证按输入顺序到达; 也就是说,稍后创建的线程可能会在情况下在较早的线程之前返回其输出。

一个简单的例子:

PS> 3, 1 | ForEach-Object -Parallel { Start-Sleep $_; "$_" }
1  # !! *Second* input's thread produced output *first*.
3

为了按输入顺序显示输出 - 这总是需要在显示输出之前等待所有线程完成,您可以添加-AsJob开关

  • 然后返回单个轻量级(基于线程)作业对象而不是直接输出,该对象返回PSTaskJob类型的单个作业, PSTaskJob包含多个作业,每个并行运行空间(线程)一个; 您可以使用通常的*-Job cmdlet 管理它,并通过.ChildJobs属性访问各个子作业。

通过等待整个作业完成,通过Receive-Job接收其输出,然后按输入顺序显示它们:

PS> 3, 1 | ForEach-Object -AsJob -Parallel { Start-Sleep $_; "$_" } |
      Receive-Job -Wait -AutoRemove
3  # OK, first input's output shown first, due to having waited.
1

[1] 或者,将您的MakeARestCall函数重新定义为过滤器函数( Filter ),通过$_隐式操作管道输入,因此您可以按原样使用其定义作为ForEach-Object -Parallel脚本块:

# Sample *filter* function that echoes the pipeline input it is given.
Filter MakeARestCall { "MakeARestCall: $_" }

# Pass the filter function's definition (which is a script block)
# directly to ForEach-Object -Parallel
'foo', 'bar' | ForEach-Object -Parallel $function:MakeARestCall

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM