gpt4 book ai didi

powershell - 无法与foreach对象并行开始作业

转载 作者:行者123 更新时间:2023-12-02 23:17:41 27 4
gpt4 key购买 nike

我已经准备好此脚本以尝试使用不同的参数多次并行执行相同的函数:

$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对象,并用没有Start-Job的常规顺序foreach块替换了它,则将正确调用Web服务。这就是我尝试并行运行该块时遇到的问题。

我究竟做错了什么?

最佳答案

后台作业在独立的子进程中运行,这些子进程实际上与调用方不共享任何状态;特别:

  • 他们看不到在调用 session 中定义的功能和别名,也看不到手动导入的模块,也没有手动加载的.NET程序集。
  • 他们不会加载(点源)您的$PROFILE文件,因此他们从那里看不到任何定义。
  • 在PowerShell 6.x及更低版本(包括Windows PowerShell)中,甚至没有从调用方继承当前位置(目录)(默认为[Environment]::GetFolderPath('MyDocuments'));这已在v7.0中修复。
  • 他们确实看到的调用 session 状态的唯一方面是调用进程的环境变量的副本。
  • 要使调用者 session 中的变量值可用于后台作业,必须通过$using:scope对其进行引用(请参阅 about_Remote_Variables )。
  • 请注意,对于字符串,原始类型(例如数字)和少数其他知名类型以外的其他值,这可能会导致类型保真度降低,因为使用PowerShell的基于XML的序列化会跨进程边界对这些值进行编码和反序列化;这种潜在的类型保真度损失也会影响作业的输出-有关背景信息,请参见this answer
  • 通过 Start-ThreadJob 使用更快,更省资源的线程作业可以避免此问题(尽管所有其他限制都适用); Start-ThreadJob随PowerShell [Core] 6+一起提供,可以在Windows PowerShell中按需安装(例如Install-Module -Scope CurrentUser ThreadJob)-有关背景信息,请参见this answer

  • 重要:每当您使用作业自动化时,例如在Windows Task Scheduler调用的脚本中或在CI / CD的上下文中,请确保在退出脚本之前要等待所有作业完成(通过 Receive-Job -Wait Wait-Job ),因为通过PowerShell的CLI调用的脚本会整体退出PowerShell进程,从而杀死所有不完整的作业。

    因此,除非命令MakeARestCall:
  • 恰好是脚本文件(MakeARestCall.ps1)或可执行文件(MakeARestCall.exe),位于$env:Path
  • 中列出的目录之一中
  • 恰好是在自动加载的模块
  • 中定义的函数

    如果既未定义$doJob函数也未定义别名,则在作业过程中执行时,您的MakeARestCall脚本块将失败。

    您的评论表明 MakeARestCall确实是一个函数,因此,为了使您的代码正常工作,您必须(重新)将该函数定义为作业(在您的情况下为$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
    当然,如果您不介意复制函数定义或仅在后台作业的上下文中需要它,则可以直接将函数定义直接嵌入脚本块中。

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

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

    从本质上讲,是使用线程作业(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类型的单个作业,该作业包含多个子作业,每个并行运行空间(线程)一个。您可以使用常规的*-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]或者,通过MakeARestCallFilter函数重新定义为对管道输入隐式操作的过滤器函数($_),因此您可以按其原样使用其定义作为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

    关于powershell - 无法与foreach对象并行开始作业,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/60668611/

    27 4 0
    Copyright 2021 - 2024 cfsdn All Rights Reserved 蜀ICP备2022000587号
    广告合作:1813099741@qq.com 6ren.com