Home > Net >  Foreach-Object -Parallel returning "The term 'write-host' is not recognized as a name
Foreach-Object -Parallel returning "The term 'write-host' is not recognized as a name

Time:10-31

I've encountered this issue in a longer script and have simplified here to show the minimal code required to reproduce it (I think). It outputs numbers followed by letters: 1 a 1 b 1 c... 2 a 2 b 2 c... all the way to "500 z"

Function Write-HelloWorld
{
    Param($number)
    write-host -Object $number
}

$numbers = 1..500
$letters = "a".."z"
$Function = get-command Write-HelloWorld 
$numbers | ForEach-Object -Parallel {
    ${function:Write-HelloWorld} = $using:Function
    foreach($letter in $using:letters) {
        Write-HelloWorld -number "$_ $letter"
    }
}

I'm seeing 2 types of sporadically (not every time I run it):

  1. "The term 'write-host' is not recognized as a name of a cmdlet, function, script file, or executable program." As understand it, write-host should always be available. Adding the line "Import-Module Microsoft.PowerShell.Utility" just before the call to write-host didn't help
  2. Odd output like the below, specifically all the "write-host :" lines.

enter image description here

CodePudding user response:

Can't give you a proper answer as to why this is happening but clearly, it's a very bad idea to pass in a reference object and use it without thread safety, here is proof that simply adding thread safety to your code the problem is solved:

function Write-HelloWorld {
    param($number)
    Write-Host -Object $number
}

$numbers  = 1..500
$letters  = "a".."z"
$Function = Get-Command Write-HelloWorld

$numbers | ForEach-Object -Parallel {
    $refObj = $using:Function

    [System.Threading.Monitor]::Enter($refObj)

    ${function:Write-HelloWorld} = $using:Function
    foreach($letter in $using:letters) {
        Write-HelloWorld -number "$_ $letter"
    }

    [System.Threading.Monitor]::Exit($refObj)
}

CodePudding user response:

Santiago Squarzon has provided the crucial pointer:

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*
$funcDefString = ${function:Write-HelloWorld}.ToString()
$numbers | ForEach-Object -Parallel {
  # Redefine the Write-HelloWorld in this function 
  # using the *string* representation of its body.
  ${function:Write-HelloWorld} = $using:funcDefString
    foreach($letter in $using:letters) {
        Write-HelloWorld -number "$_ $letter"
    }
}

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), 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.


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.
  • Related