解决方案并不像人们希望的那样直截了当:
# Sample custom function.
function Get-Custom {
Param ($A)
"[$A]"
}
# Get the function's definition *as a string*
$funcDef = ${function:Get-Custom}.ToString()
"Apple", "Banana", "Grape" | ForEach-Object -Parallel {
# Define the function inside this thread...
${function:Get-Custom} = $using:funcDef
# ... and call it.
Get-Custom $_
}
注意: 此答案提供了一个类似的解决方案,用于在ForEach-Object -Parallel
脚本块中使用调用者范围内的脚本块。
注意: 如果您的函数定义在已知模块自动加载功能的任一位置中的模块中,则您的函数调用将直接与ForEach-Object -Parallel
一起工作,无需额外努力-但每个线程都会产生(隐式)导入模块的成本。
上述方法是必要的,因为除了当前位置(工作目录)和环境变量(适用于整个进程)之外,ForEach-Object -Parallel
创建的线程不会看到调用者的状态,特别是对于变量和函数(以及自定义PS驱动器和导入的模块)。
截至PowerShell 7.2.x,正在讨论GitHub问题#12240的增强功能,以支持将调用者的状态按需复制到并行线程中,这将使调用者的函数自动可用。
请注意,通过字符串在每个线程中重新定义函数至关重要,因为尝试在没有辅助$funcDef
变量的情况下进行操作,并尝试使用${function:Get-Custom} = ${using:function:Get-Custom}
重新定义函数失败,因为${function:Get-Custom}
是一个脚本块,而使用带有$using:
范围限定符的脚本块被明确禁止,以避免跨线程(跨运行空间)问题。
然而,${function:Get-Custom} = ${using:function:Get-Custom}
可以在 Start-Job
中使用;请参见此答案中的示例。
它无法在 Start-ThreadJob
中工作,尽管语法上允许您执行 & ${using:function:Get-Custom} $_
,因为 ${using:function:Get-Custom}
被保留为一个脚本块(与 Start-Job
不同,后者将其反序列化为字符串,这是令人惊讶的行为 - 请参见GitHub issue #11698),但实际上不能。也就是说,直接跨线程使用 [scriptblock]
实例会导致模糊的故障,这就是为什么 ForEach-Object -Parallel
在第一次使用时就防止了它。
一个类似的漏洞,即在每个线程中使用从调用方范围中使用 Get-Command
获取的命令信息对象作为函数体,通过 $using:
范围传递:这也应该被阻止,但自 PowerShell 7.2.7 以来没有这样做 - 请参见 此帖子 和 GitHub issue #16461。
${function:Get-Custom}
是 命名空间变量表示法 的实例,它允许您同时 获取 函数(其 主体 作为 [scriptblock]
实例)和 定义 它,通过分配一个 [scriptblock]
或包含函数主体的字符串。
ForEach-Object -Parallel
块中使用,那么这种技术是非常有用的;直接插入函数定义可能更快,但我不确定在实践中是否有太大的区别。 - mklement0ForEach-Object -Parallel {
# Include custom functions inside parallel scope
. $using:PSScriptRoot\CustomFunctions.ps1
# Now you can reference any function defined in the file
My-CustomFunction
....
这确实会产生开销,需要在每个并行进程中加载函数,但在我的情况下,与整体处理时间相比,这是微不足道的。
所以我发现了另一个小技巧,可能对那些试图动态添加函数的人很有用,特别是当你事先不知道它的名称时,比如当这些函数在数组中时。
# Store the current function list in a variable
$initialFunctions=Get-ChildItem Function:
# Source all .ps1 files in the current folder and all subfolders
Get-ChildItem . -Recurse | Where-Object { $_.Name -like '*.ps1' } |
ForEach-Object { . "$($_.FullName)" }
# Get only the functions that were added above, and store them in an array
$functions = @()
Compare-Object $initialFunctions (Get-ChildItem Function:) -PassThru |
ForEach-Object { $functions = @($functions) + @($_) }
1..3 | ForEach-Object -Parallel {
# Pull the $functions array from the outer scope and set each function
# to its definition
$using:functions | ForEach-Object {
Set-Content "Function:$($_.Name)" -Value $_.Definition
}
# Call one of the functions in the sourced .ps1 files by name
SourcedFunction $_
}
Set-Content
和Function:
加上函数名称,因为PowerShell本质上将Function:
的每个条目都视为路径。Get-PSDrive
的输出时,这是有意义的。由于这些条目中的每一个都可以以相同的方式(即带有冒号)用作“驱动器”。$_.Definition
返回一个字符串。然而,这里有一个警告:使用[System.Management.Automation.FunctionInfo]
实例(通过Get-ChildItem Function:
或Get-Command
获得)本质上是有问题的,因为它们确实包含一个脚本块(.ScriptBlock
),甚至可以直接被调用,使用&
。这样的调用可能会导致状态损坏,就像GitHub问题#16461中所解释的那样,该问题主张通过$using:
禁止使用FunctionInfo
。 - undefined我刚刚想到了另一种使用get-command的方式,可以与调用运算符一起使用。$a最终会成为一个FunctionInfo对象。
编辑:有人告诉我这不是线程安全的,但我不明白为什么。
function hi { 'hi' }
$a = get-command hi
1..3 | foreach -parallel { & $using:a }
hi
hi
hi
Foreach-Object -Parallel
块中:$m = New-Module -Name MyFunctions -ScriptBlock {
function Func-Timestamp {
return [DateTime]::Now.ToString("HH:mm:ss.ff")
}
}
$files | ForEach-Object -Parallel {
import-module $using:m -DisableNameChecking
[Console]::WriteLine("Here is the Time! $(Func-TimeStamp)")
}
$m
的模块变量,然后将其导入到Foreach-Object
循环中。
-Parallel
块内(重新)定义它。 - Mathias R. JessenWrite-Host
通常不是正确的工具,除非意图仅仅是写入到显示器上,绕过成功输出流以及将输出发送到其他命令、在变量中捕获它或将其重定向到文件的能力。要输出一个值,请直接使用它;例如,使用$value
而不是Write-Host $value
(或者使用Write-Output $value
,但这很少需要)。另请参见:https://dev59.com/0Zvga4cB1Zd3GeqP9vTJ#50416448的底部部分。 - mklement0