How to pass a custom function inside a ForEach-Object -Parallel
I just figured out another way using get-command, which works with the call operator. $a ends up being a FunctionInfo object. EDIT: I'm told this isn't thread safe, but I don't understand why.
function hi { 'hi' }
$a = get-command hi
1..3 | foreach -parallel { & $using:a }
hi
hi
hi
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).- Update: js2010's helpful answer shows a more straightforward solution that passes a
System.Management.Automation.FunctionInfo
instance, obtained viaGet-Command
, which can be invoked directly with&
. The only caveat is that the original function should be side-effect-free, i.e. should operate solely based on parameter or pipeline inputs, without relying on the caller's state, notably its variables, as that could lead to thread-safety issues. The stringification technique above implicitly prevents any problematic references to the caller's state, because the function body is rebuilt in each thread's context.
- Update: js2010's helpful answer shows a more straightforward solution that passes a
As of PowerShell 7.1, an enhancement is being discussed in GitHub issue #12240 to support copying the caller's state to the threads on demand, which would make the caller's functions available.
Note that making do without the aux. $funcDef
variable and trying to redefine the function with ${function:Get-Custom} = ${using:function:Get-Custom}
is tempting, but ${function:Get-Custom}
is a script block, and the use of script blocks with the $using:
scope specifier is explicitly disallowed.
However,
${function:Get-Custom} = ${using:function:Get-Custom}
would work withStart-Job
; see this answer for an example.It would also work with
Start-ThreadJob
, where you could even 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). However, it is unclear whether this behavior is supported by design, because it is subject to the same potential cross-thread issues noted above.
${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.