ForEach-Object -Parallel
会在一个单独的运行空间中执行循环体,这意味着您无法直接访问调用范围中定义的变量。
为了解决这个问题,请对代码进行两个更改:
- 使用除可调整大小数组之外的集合类型(下面我使用了通用的
[List[psobject]]
)
- 使用
using:
范围修饰符从调用者的作用域引用变量,并将其分配给块内的本地变量
结果本地变量将引用相同的列表对象,通过其方法(Add()
、Remove()
、AddRange()
等)对该列表所做的更改将反映在任何其他引用它的地方(包括来自调用范围的原始 $DBInventory
变量)。
$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)"
如
mklement0所指出的,
[List[psobject]]
是
不安全的线程 - 对于生产代码,您肯定需要选择一种是线程安全的集合类型,例如
[System.Collections.Concurrent.ConcurrenBag[psobject]]
- 本质上是一个无序列表:
$DBInventory = [System.Collections.Concurrent.ConcurrentBag[psobject]]::new()
请注意,ConcurrentBag
类型并不像其名称所示那样保留插入顺序。如果这是一个问题,您可能需要考虑使用[ConcurrentDictionary[string,psobject[]]]
- 这样您就可以将查询结果与原始输入字符串绑定:
$DBInventory = [System.Collections.Concurrent.ConcurrentDictionary[string,psobject[]]]::new()
由于另一个线程可能(假设)在您调用Add()
之后为相同的键添加了一个条目,因此ConcurrentDictionary
类型要求我们稍微与常规字典或哈希表不同地使用它:
$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)"
}
在这里,如果键不存在(否则将运行$updater
),并且结果将被分配为条目值,则并发字典将代表我们执行$adder
函数。
您随后可以以与哈希表相同的方式访问条目值:
$DBInventory[$vms[-1]]