2

In Powershell, is there a way to increment a variable from multiple threads safely.

I thought the below code using System.Threading.Interlock::Add would work, but the output shows thread mashing is occurring.

# Threads will attempt to increment this number
$testNumber = 0

# Script will increment the test number 10 times
$script =  {
    Param(
        [ref]$testNumber
    )
    1..10 | % {
        [System.Threading.Interlocked]::Add($testNumber, 1) | Out-Null
    }
}

#  Start 10 threads to increment the same number
$threads = @()
1..10 | % {
    $ps = [powershell]::Create()
    $ps.AddScript($script) | Out-Null
    $ps.RunspacePool = $pool
    $ps.AddParameter('testNumber', [ref]$testNumber) | Out-Null
    
    $threads += @{
        ps = $ps
        handle = $ps.BeginInvoke()
    }
}

# Wait for threads to complete
while ($threads | ? {!$_.Handle.IsCompleted }) {
    Start-Sleep -Milliseconds 100
}

# Print the output (should be 100=10*10, but is between 90 and 100)
echo $testNumber
mdsimmo
  • 559
  • 5
  • 16
  • I quess you need to create a synchronized hash table, e.g.: `[hashtable]::Synchronized(@{ TestNumber = 0 })`, see e.g.: https://stackoverflow.com/a/36716964/1701026 – iRon Oct 15 '20 at 09:11
  • 1
    `$pool` is undefined a variable. A copy-paste error? – vonPryz Oct 15 '20 at 09:17

1 Answers1

0

As iRon mentions in the comments, you'll want to use a synchronized hashtable for reading and writing data across the runspace boundary (see inline comments for explanation):

# Threads will attempt to increment this number
$testNumber = 0

# Create synchronized hashtable to act as gatekeeper
$syncHash = [hashtable]::Synchronized(@{ testNumber = $testNumber })

# Create runspace pool with a proxy variable mapping back to $syncHash
$iss = [initialsessionstate]::CreateDefault2()
$iss.Variables.Add(
  [System.Management.Automation.Runspaces.SessionStateVariableEntry]::new('syncHash', $syncHash,'')
)
$pool = [runspacefactory]::CreateRunspacePool($iss)
$pool.Open()

# Script will increment the test number 10 times
$script =  {
    Param(
        [string]$targetValue
    )
    1..10 | % {
        # We no longer need Interlocked.* and hairy [ref]-casts, $syncHash is already thread safe
        $syncHash[$targetValue]++ 
    }
}

#  Start 10 threads to increment the same number
$threads = @()
1..10 | % {
    $ps = [powershell]::Create()
    $ps.AddScript($script) | Out-Null
    $ps.RunspacePool = $pool

    # We're no longer passing a [ref] to the variable in the calling runspace.
    # Instead, we target the syncHash entry by name
    $ps.AddParameter('targetValue', 'testNumber') | Out-Null
    
    $threads += @{
        ps = $ps
        handle = $ps.BeginInvoke()
    }
}

# Wait for threads to complete
while ($threads | ? {!$_.Handle.IsCompleted }) {
    Start-Sleep -Milliseconds 100
}

$errorCount = 0
# End invocation lifecycle
$threads|%{
  if($_.ps.HadErrors){
    $errorCount++
  }
  $_.ps.EndInvoke($_.handle)
}

if($errorCount){
  Write-Warning "${errorCount} thread$(if($errorCount -gt 1){'s'}) had errors"
}

# Et voila, $syncHash['testNumber'] is now 100
$syncHash['testNumber']
Mathias R. Jessen
  • 157,619
  • 12
  • 148
  • 206