后台作业在独立的子进程中运行,与调用者几乎不共享任何状态;具体来说:
他们看不到调用会话中定义的任何函数和别名,也看不到手动导入的模块,也看不到手动加载的 .NET 程序集。
他们不会加载(点源)您的$PROFILE
文件,因此他们不会从那里看到任何定义。
在 PowerShell 版本 6.x 及更低版本(包括 Windows PowerShell)中,甚至当前位置(目录)都不是从调用者继承的(它默认为[Environment]::GetFolderPath('MyDocuments')
);这已在 v7.0 中修复。
他们看到的调用会话状态的唯一方面是调用进程的环境变量的副本。
要使调用者会话中的变量值可用于后台作业,必须通过$using:scope
(请参阅参考资料about_Remote_Variables
)引用它们。
- 请注意,对于字符串、原始类型(例如数字)和少数其他知名类型以外的值,这可能会导致类型保真度的损失,因为这些值是使用 PowerShell 的基于 XML 的序列化和跨进程边界编组的。反序列化;这种类型保真度的潜在损失也会影响工作的输出- 有关背景信息,请参阅此答案。
- 使用速度更快、资源消耗更少的线程作业 via
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
:
在作业过程中执行时,您的$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-Object -Parallel
:
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
开关:
- 然后返回一个单一的、轻量级(基于线程的)作业对象,而不是直接输出,该对象返回一个包含多个子作业的类型的作业,每个并行运行空间(线程)一个;您可以使用常用的 cmdlet 对其进行管理,并通过该属性访问各个子作业。
PSTaskJob
*-Job
.ChildJobs
通过等待整个工作完成Receive-Job
,通过then接收其输出以输入顺序显示它们:
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
),它隐式地对管道输入进行操作, via $_
,因此您可以将其定义用作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