ForEach-Object -Parallel
causes execution of the loop body in a separate runspace, meaning you don't have direct access to the variables defined in the calling scope.
To work around this, make two changes to your code:
- Use a collection type other than a resizable array (below I've use a generic
[List[psobject]]
)
- Reference the variable from the caller's scope with the
using:
scope modifier and assign to a local inside the block
The resulting local variable will then reference the same list-object in memory, and changes made to that list via its methods (Add()
, Remove()
, AddRange()
, etc.) will be reflected anywhere else its referenced (including the original $DBInventory
variable from your calling scope).
$DBDetails = "SELECT @@VERSION"
$VMs = ("vm1", "vm2", "vm3", "vm4", "vm5", "vm6", "vm7")
$DBInventory = [System.Collections.Generic.List[psobject]]::new()
$scriptBlock = {
$vm = $_
$inventory = $using:DBInventory
$result = Invoke-Sqlcmd -ServerInstance $vm -Query $using:DBDetails
$inventory.AddRange([psobject[]]$result)
Write-Host "Added $($result.Count) rows from $($vm)"
}
$VMs | ForEach-Object -Parallel $scriptBlock
Write-Host "Number of elements in DBInventory: $($DBInventory.Count)"
As mklement0 notes, [List[psobject]]
is not thread-safe - for production code you'll definitely want to pick a collection type that is, like for example a [System.Collections.Concurrent.ConcurrenBag[psobject]]
- essentially an unordered list:
$DBInventory = [System.Collections.Concurrent.ConcurrentBag[psobject]]::new()
Beware that the ConcurrentBag
type, as the name might suggest, does not preserve insertion order. If this is a problem, you may want to consider using a [ConcurrentDictionary[string,psobject[]]]
- this way you can tie the query output back to the orignal input string:
$DBInventory = [System.Collections.Concurrent.ConcurrentDictionary[string,psobject[]]]::new()
Since another thread may (hypothetically) have added an entry for the same key since you dispatched your call to Add()
, the ConcurrentDictionary
type requires us to use it slightly differently than a regular dictionary or hashtable:
$scriptBlock = {
$vm = $_
$inventory = $using:DBInventory
$result = Invoke-Sqlcmd -ServerInstance $vm -Query $using:DBDetails
$adder = $updater = { return Write-Output $result -NoEnumerate }
$inventory.AddOrUpdate($vm, $adder, $updater)
Write-Host "Added $($result.Count) rows from $($vm)"
}
Here, the concurrent dictionary will execute the $adder
function on our behalf if the key doesn't already exist (otherwise it'll run the $updater
), and the result will be assigned as the entry value.
You can subsequently access the entry values the same way you would a hashtable:
$DBInventory[$vms[-1]] # returns array containing the query results from the last VM in the list