簡體   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