如何在 ForEach-Object -Parallel 中传递自定义函数

How to pass a custom function inside a ForEach-Object -Parallel

我找不到传递函数的方法。只是变量。

有没有将函数放在 ForEach 循环中的想法?

function CustomFunction {
    Param (
        $A
    )
    Write-Host $A
}

$List = "Apple", "Banana", "Grape" 
$List | ForEach-Object -Parallel {
    Write-Host $using:CustomFunction $_
}

解决方案并不像人们希望的那么简单:

# 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 脚本块中使用来自调用方范围的 脚本块

  • 注意:如果您的函数是在 模块 中定义的,该模块位于已知的位置之一module-autoloading 功能,您的函数调用将按原样与 ForEach-Object -Parallel 一起工作,无需额外的努力 - 但每个线程都会产生(隐式)导入模块的成本。

  • 上述方法是必要的,因为 - 除了当前位置(工作目录)和环境变量(适用于进程范围) - ForEach-Object -Parallel 创建的线程执行 看不到调用者的状态,尤其是在变量和函数方面都看不到(而且也看不到自定义 PS 驱动器和导入模块)。

    • 更新: shows a more straightforward solution that passes a System.Management.Automation.FunctionInfo instance, obtained via Get-Command,可以直接用&调用。唯一需要注意的是,原始函数应该是无副作用的,即应该仅基于参数或管道输入运行,而不依赖于调用者的状态,尤其是它的变量,因为这可能会导致线程安全问题。上面的字符串化技术隐含地防止了对调用者状态的任何有问题的引用,因为函数体是在每个线程的上下文中重建的。
  • 从 PowerShell 7.1 开始,GitHub issue #12240 中正在讨论一项增强功能,以支持将调用者的状态按需 复制到线程,这将使调用者的功能可用。

请注意,没有辅助功能也能凑合。 $funcDef 变量并尝试用 ${function:Get-Custom} = ${using:function:Get-Custom} 重新定义函数很诱人,但 ${function:Get-Custom} 是一个 脚本块 ,并且使用脚本块与$using: 范围说明符被明确禁止。

  • 但是,${function:Get-Custom} = ${using:function:Get-Custom} Start-Job; see 一起工作。

  • 它也适用于 Start-ThreadJob, where you could even do & ${using:function:Get-Custom} $_, because ${using:function:Get-Custom} is preserved as a script block (unlike with Start-Job, where it is deserialized as a string, which is itself surprising behavior - see GitHub issue #11698)。但是,尚不清楚设计是否支持此行为,因为它会遇到上述相同的潜在跨线程问题。

${function:Get-Custom} 的一个实例,它允许您同时 get 一个函数(它的 body 作为 [scriptblock] 实例)并 设置 (定义)它,通过分配 [scriptblock] 或包含函数主体的字符串。

我刚刚想出了另一种使用 get-command 的方法,它与呼叫操作员一起使用。 $a 最终成为一个 FunctionInfo 对象。编辑:有人告诉我这不是线程安全的,但我不明白为什么。

function hi { 'hi' }
$a = get-command hi
1..3 | foreach -parallel { & $using:a }

hi
hi
hi

所以我想出了另一个小技巧,它可能对尝试动态添加函数的人有用,特别是如果您事先可能不知道它的名称,例如当函数在数组中时。

# 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-ContentFunction: 加上函数名称,因为 PowerShell 本质上将 Function: 的每个条目视为一个路径。

当您考虑 Get-PSDrive 的输出时,这是有道理的。由于这些条目中的每一个都可以以相同的方式(即使用冒号)用作“驱动器”。

如果您是专业人士,您当然会故意添加 -Parallel 标志,因为您确实需要并行处理(因此请参阅已接受的答案)

像我这样的新手可能会考虑删除 -Parallel 标志,因为你没有意识到你从其他地方复制的代码并不真正需要它..然后你的函数调用就像正常一样工作.