multithreadingpowershellparallel-processingrunspaceforeach-object

How to exit from ForEach-Object Parallel and update a variable outside of Runspace in Powershell 7?


The code below isolates the task I am trying to do. Once a number over 3 is checked in $loopMe, I want the foreach-object loop to end across all runspaces and that the value of numberOverLimit is set to true.

$loopMe = [System.Collections.ArrayList]@()
for($index = 0; $index -lt 5; $index++){
  $loopMe.Add($index)>$null;
}
$global:numberOverLimit=$false
$addToMe= [System.Collections.Concurrent.ConcurrentBag[psobject]]::new() 
$loopMe | Foreach-Object -ThrottleLimit 6 -Parallel{
  $localAddToMe=$using:addToMe
  Write-Host $_
  if($_ -gt 3){
    $global:numberOverLimit=$true
    break
  }
  $localAddToMe.Add($_)
}
write-Host $addToMe
Write-Host $numberOverLimit
if($numberOverLimit){
    Write-Host "A number was over the limit"
    exit
}
else{
    Write-Host "All is good"
}

Expected Result

0
1
2
3
3 2 1 0
True
A number was over the limit

Actual Result

0
1
4
2
3
3 2 1 0
False
All is good

Solution

  • Your parallel loop can't see your $global:numberOverLimit variable and even if it would, it wouldn't be able to update the boolean (a value type). You can only update a reference type from your parallel loop, which is why I'm using Get-Variable in this example.

    Also note, the use of break in a script block:

    Using break inside a pipeline break, such as a ForEach-Object script block, not only exits the pipeline, it potentially terminates the entire runspace.

    The only built-in method to stop a pipeline early is with Select-Object -First, you can pipe your parallel loop to it and output anything to stdout to terminate your parallel loop.

    Lastly, you should ensure thread safety before updating your PSVariable instance, for that you need to use some sort of locking mechanism, Monitor.Enter is the one used in this case. See this answer for other alternatives.

    $addToMe = [System.Collections.Concurrent.ConcurrentBag[psobject]]::new()
    $numberOverLimit = $false
    $psvar = Get-Variable numberOverLimit
    
    0..5 | ForEach-Object -ThrottleLimit 6 -Parallel {
        $localAddToMe = $using:addToMe
        $psvar = $using:psvar
    
        Write-Host $_
    
        if ($_ -gt 3) {
            [System.Threading.Monitor]::Enter($psvar)
            $psvar.Value = $true
            [System.Threading.Monitor]::Exit($psvar)
            return 'something, whatever here'
        }
    
        $localAddToMe.Add($_)
    } | Select-Object -First 1 | Out-Null
    
    Write-Host $addToMe
    Write-Host $numberOverLimit
    
    if ($numberOverLimit) {
        Write-Host 'A number was over the limit'
        return
    }
    else {
        Write-Host 'All is good'
    }
    

    Another alternative that achieves the same as above but makes the code simpler can be to use a ManualResetEventSlim:

    $addToMe = [System.Collections.Concurrent.ConcurrentBag[psobject]]::new()
    $resetSlim = [System.Threading.ManualResetEventSlim]::new()
    
    0..5 | ForEach-Object -ThrottleLimit 6 -Parallel {
        $localAddToMe = $using:addToMe
        $localResetSlim = $using:resetSlim
    
        Write-Host $_
    
        if ($_ -gt 3) {
            $localResetSlim.Set()
            return 'something, whatever here'
        }
    
        $localAddToMe.Add($_)
    } | Select-Object -First 1 | Out-Null
    
    # this type should always be disposed when no longer needed
    $resetSlim.Dispose()
    
    Write-Host $addToMe
    Write-Host $numberOverLimit
    
    if ($resetSlim.IsSet) {
        Write-Host 'A number was over the limit'
        return
    }
    else {
        Write-Host 'All is good'
    }