[英]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
)。
Start-ThreadJob
使用速度更快、資源占用更少的線程作業可以避免這個問題(盡管所有其他限制都適用); Start-ThreadJob
隨 PowerShell [Core] 6+ 一起提供,並且可以在 Windows PowerShell 中按需安裝(例如, Install-Module -Scope CurrentUser ThreadJob
) - 有關背景信息,請參閱此答案。 重要提示:每當您將作業用於自動化時,例如在從 Windows 任務計划程序調用的腳本中或在 CI/CD 的上下文中,請確保在退出腳本之前等待所有作業完成(通過Receive-Job -Wait
或Wait-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: foo
和MakeARestCall: bar
,表明在作業的過程中成功調用了(重新定義的) MakeARestCall
函數。
另一種方法:
讓MakeARestCall
腳本( MakeARestCall.ps1
)和呼叫通過其完整路徑,是安全的。
例如,如果您的腳本與調用腳本位於同一文件夾中,則將其調用為& $using:PSScriptRoot\\MakeARestCall.ps1 -MyParam $myParam
當然,如果您不介意復制函數定義或僅在后台作業的上下文中需要它,您可以簡單地將函數定義直接嵌入到腳本塊中。
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.