In PowerShell when updating a single value from multiple threads you must use a locking mechanism, for example Mutex
, SemaphoreSlim
or even Monitor.Enter
otherwise the updating operation will not be thread safe. A synchronized hashtable does not ensure that updating the key values is thread safe.
Below is a simple demo that proves what is stated above:
$sync = [hashtable]::Synchronized(@{ })
$attempts = 0
do {
$sync['Value'] = 0
$attempts++
0..10 | ForEach-Object -Parallel {
$sync = $using:sync
Start-Sleep -Milliseconds 200
$sync['Value']++
} -ThrottleLimit 11
}
while ($sync['Value'] -eq 11)
"It took $attempts attempts to fail..."
Supposing we have an array of arrays:
$toProcess = 0..10 | ForEach-Object {
, (Get-Random -Count (Get-Random -Minimum 5 -Maximum 10))
}
And you wanted to keep track of the processed items in each array, here is how you could do it using Mutex
:
$processedItems = [hashtable]::Synchronized(@{
Lock = [System.Threading.Mutex]::new()
Counter = 0
})
$toProcess | ForEach-Object -Parallel {
# using sleep as to emulate doing something here
Start-Sleep (Get-Random -Maximum 5)
# bring the local variable to this scope
$ref = $using:processedItems
# lock this thread until I can write
if($ref['Lock'].WaitOne()) {
# when I can write, update the value
$ref['Counter'] += $_.Count
# and realease this lock so others threads can write
$ref['Lock'].ReleaseMutex()
}
}
$processedCount = ($toProcess | Write-Output | Measure-Object).Count
# Should be True:
$processedItems['Counter'] -eq $processedCount
Another example of tread safe incrementing a counter using Monitor.Enter
and a custom function that tries to resemble the C# lock
statement:
function lock {
param(
[Parameter(Mandatory)]
[object] $Object,
[Parameter(Mandatory)]
[scriptblock] $ScriptBlock
)
try {
[System.Threading.Monitor]::Enter($Object)
& $ScriptBlock
}
finally {
[System.Threading.Monitor]::Exit($Object)
}
}
$utils = [hashtable]::Synchronized(@{
LockFunc = $function:lock.ToString()
Counter = @(0)
})
$toProcess | ForEach-Object -Parallel {
# bring the utils var to this scope
$utils = $using:utils
# define the `lock` function here
$function:lock = $utils['LockFunc']
Start-Sleep (Get-Random -Maximum 5)
# lock the counter array
lock($utils['Counter'].SyncRoot) {
# increment and release when done
$utils['Counter'][0] += $_.Count
}
}
$processedCount = ($toProcess | Write-Output | Measure-Object).Count
# Should be True:
$utils['Counter'][0] -eq $processedCount
A much simpler approach in PowerShell would be to output from your parallel loop into a linear loop where you can safely update the counter without having to care about thread safety:
$counter = 0
$toProcess | ForEach-Object -Parallel {
# using sleep as to emulate doing something here
Start-Sleep (Get-Random -Maximum 5)
# when this thread is done,
# output this array of processed items
$_
} | ForEach-Object {
# then the output from the parallel loop is received in this linear
# thread safe loop where we can update the counter
$counter += $_.Count
}
$processedCount = ($toProcess | Write-Output | Measure-Object).Count
# Should be True:
$counter -eq $processedCount