Powershell 无法使用foreach对象并行启动作业

Powershell 无法使用foreach对象并行启动作业,powershell,parallel-processing,start-job,foreach-object,Powershell,Parallel Processing,Start Job,Foreach Object,我准备了这个脚本,试图用不同的参数并行执行同一个函数多次: $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 = "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对象并用正常的顺序foreach块替换它,而不启动作业,则会正确调用webservices。这意味着,当我尝试并行运行块时,我的问题将得到解决


我做错了什么?

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

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

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

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

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

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

    • 请注意,对于字符串、基元类型(如数字)和少数其他已知类型以外的值,这可能会导致类型保真度的损失,因为这些值是使用PowerShell基于XML的序列化和反序列化跨进程边界封送的;这种类型保真度的潜在损失也会影响作业的输出-有关背景信息,请参阅
    • 通过使用速度更快、资源密集度更低的线程作业可以避免此问题(尽管所有其他限制都适用)
      Start ThreadJob
      随PowerShell[Core]6+一起提供,可根据需要在Windows PowerShell中安装(例如,
      安装模块-Scope CurrentUser ThreadJob
      )-请参阅以获取背景信息
重要信息无论何时使用作业进行自动化,例如在从Windows任务计划程序调用的脚本中或在CI/CD上下文中,请确保在退出脚本之前等待所有作业完成(通过或),因为通过PowerShell调用的脚本将作为一个整体退出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:foo
MakeARestCall:bar
,表明在作业过程中成功调用了(重新定义的)
MakeARestCall
函数

替代方法

制作
MakeARestCall
脚本(
MakeARestCall.ps1
),并通过其完整路径调用该脚本,以确保安全

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

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


更简单、更快的PowerShell[Core]7+替代方案,使用
ForEach对象-并行
:
PowerShell 7中引入的
-Parallel
参数在每个管道输入对象的单独运行空间(线程)中运行给定的脚本块

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

但是,上文讨论的后台作业缺乏状态共享也适用于线程作业(即使它们在同一进程中运行,也在独立的PowerShell运行空间中运行),因此这里的
MakARestCall
函数也必须(重新)定义(或嵌入)在脚本块中

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

上面的代码在到达时直接从并行线程发出输出——但请注意,这意味着输出不能保证按输入顺序到达;也就是说,稍后创建的线程可能会在较早的线程之前返回其输出

一个简单的例子:

PS> 3, 1 | ForEach-Object -Parallel { Start-Sleep $_; "$_" }
1  # !! *Second* input's thread produced output *first*.
3
为了按输入顺序显示输出(通常需要等待所有线程完成后再显示输出),可以添加
-AsJob
开关

  • 而不是直接输出
    PS> 3, 1 | ForEach-Object -Parallel { Start-Sleep $_; "$_" }
    1  # !! *Second* input's thread produced output *first*.
    3
    
    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
    
    # 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