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

The solution isn’t quite as straightforward as one would hope:

# 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 $_
}

Note: This answer contains an analogous solution for using a script block from the caller’s scope in a ForEach-Object -Parallel script block.

  • Note: If your function were defined in a module that is placed in one of the locations known to the module-autoloading feature, your function calls would work as-is with ForEach-Object -Parallel, without extra effort – but each thread would incur the cost of (implicitly) importing the module.

  • The above approach is necessary, because – aside from the current location (working directory) and environment variables (which apply process-wide) – the threads that ForEach-Object -Parallel creates do not see the caller’s state, notably neither with respect to variables nor functions (and also not custom PS drives and imported modules).

  • As of PowerShell 7.2.x, an enhancement is being discussed in GitHub issue #12240 to support copying the caller’s state to the parallel threads on demand, which would make the caller’s functions automatically available.

Note that redefining the function in each thread via a string is crucial, as an attempt to make do without the aux. $funcDef variable and trying to redefine the function with ${function:Get-Custom} = ${using:function:Get-Custom} fails, because ${function:Get-Custom} is a script block, and the use of script blocks with the $using: scope specifier is explicitly disallowed in order to avoid cross-thread (cross-runspace) issues.

  • However, ${function:Get-Custom} = ${using:function:Get-Custom} would work with Start-Job; see this answer for an example.

  • It would not work with Start-ThreadJob, which currently syntactically allows you to 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), even though it shouldn’t. That is, direct cross-thread use of [scriptblock] instances causes obscure failures, which is why ForEach-Object -Parallel prevents it in the first place.

  • A similar loophole that leads to cross-thread issues even with ForEach-Object -Parallel is using a command-info object obtained in the caller’s scope with Get-Command as the function body in each thread via the $using: scope: this too should be prevented, but isn’t as of PowerShell 7.2.7 – see this post and GitHub issue #16461.

${function:Get-Custom} is an instance of namespace variable notation, which allows you to both get a function (its body as a [scriptblock] instance) and to set (define) it, by assigning either a [scriptblock] or a string containing the function body.

Leave a Comment