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 withStart-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 withStart-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 whyForEach-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 withGet-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.