Santiago Squarzon's helpful answer demonstrates the problem with your approach well and links to a GitHub issue explaining the underlying problem (runspace affinity); however, that demonstration isn't the right solution (it wasn't meant to be), as it uses explicit synchronization to allow only one thread at a time to call the function, which negates the benefits of parallelism.
As for a solution:
You must pass a string representation of your Write-HelloWorld
's function body to the ForEach-Object
-Parallel
call:
Function Write-HelloWorld
{
Param($number)
write-host -Object $number
}
$numbers = 1..500
$letters = "a".."z"
# Get the body of the Write-HelloWorld function *as a string*
# Alternative, as suggested by @Santiago:
# $funcDefString = (Get-Command -Type Function Write-HelloWorld).Definition
$funcDefString = ${function:Write-HelloWorld}.ToString()
$numbers | ForEach-Object -Parallel {
# Redefine the Write-HelloWorld function in this thread,
# using the *string* representation of its body.
${function:Write-HelloWorld} = $using:funcDefString
foreach($letter in $using:letters) {
Write-HelloWorld -number "$_ $letter"
}
}
${function:Write-HelloWorld}
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.
By passing a string, the function is recreated in the context of each thread, which avoids cross-thread issues that can arise when you pass a [System.Management.Automation.FunctionInfo]
instance, as output by Get-Command
, which contains a [scriptblock]
that is bound to the runspace in which it was defined (i.e., the caller's; that script block is said to have affinity with the caller's runspace), and calling this bound [scriptblock]
instance from other threads (runspaces) isn't safe.
By contrast, by redefining the function in each thread, via a string, a thread-specific [scriptblock]
instance bound to that thread is created, which can safely be called.
In fact, you appear to have found a loophole, given that when you attempt to use a [scriptblock]
instance directly with the $using:
scope, the command by design breaks with an explicit error message:
A ForEach-Object -Parallel using variable cannot be a script block.
Passed-in script block variables are not supported with ForEach-Object -Parallel,
and can result in undefined behavior
In other words: PowerShell shouldn't even let you do what you attempted to do, but unfortunately does, as of PowerShell Core 7.2.7, resulting in the obscure failures you saw - see GitHub issue #16461.
Potential future improvement:
- An enhancement is being discussed in GitHub issue #12240 to support copying the caller's state to the parallel threads on demand, which would automatically make the caller's functions available, without the need for manual redefinition.