Powershell:为什么使用 ForEach-Object -Parallel 后我的变量为空?

Art*_*lle 5 powershell foreach-object

我正在尝试使用 ForEach-Object -Parallel 从多个服务器收集数据。我使用的变量正在循环内填充,但是当循环完成时该变量为空。

$DBDetails = "SELECT @@VERSION"

$VMs = ("vm1", "vm2", "vm3", "vm4", "vm5", "vm6", "vm7")
$DBInventory = @()

$scriptBlock = {
    $vm = $_
    $result = Invoke-Sqlcmd -ServerInstance $vm -Query $using:DBDetails
    $DBInventory += $result
    Write-Host "Added $($result.Count) rows from $($vm)"
}

$VMs | ForEach-Object -Parallel $scriptBlock
Write-Host "Number of elements in DBInventory: $($DBInventory.Count)"
Run Code Online (Sandbox Code Playgroud)

我希望最后一行返回在前一行执行的循环中收集的元素数量。总共应该有 7 个元素,但我一个都没有。

我的结果如下所示:

Added 1 rows from vm1
Added 1 rows from vm2
Added 1 rows from vm3
Added 1 rows from vm4
Added 1 rows from vm5
Added 1 rows from vm6
Added 1 rows from vm7
Number of elements in DBInventory: 0
Run Code Online (Sandbox Code Playgroud)

Mat*_*sen 3

ForEach-Object -Parallel导致循环体在单独的运行空间中执行,这意味着您无法直接访问调用范围中定义的变量。

要解决此问题,请对代码进行两处更改:

  • 使用可调整大小的数组以外的集合类型(下面我使用了 generic [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)"
Run Code Online (Sandbox Code Playgroud)

正如mklement0 所指出的那样[List[psobject]]不是线程安全的- 对于生产代码,您肯定会想要选择一个集合类型,例如 a [System.Collections.Concurrent.ConcurrenBag[psobject]]- 本质上是一个无序列表:

$DBInventory = [System.Collections.Concurrent.ConcurrentBag[psobject]]::new()
Run Code Online (Sandbox Code Playgroud)

请注意,ConcurrentBag顾名思义,该类型不保留插入顺序。如果这是一个问题,您可能需要考虑使用[ConcurrentDictionary[string,psobject[]]]- 这样您可以将查询输出绑定回原始输入字符串:

$DBInventory = [System.Collections.Concurrent.ConcurrentDictionary[string,psobject[]]]::new()
Run Code Online (Sandbox Code Playgroud)

由于自从您将调用分派到 以来,另一个线程可能(假设)已经添加了同一键的条目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)"
}
Run Code Online (Sandbox Code Playgroud)

$adder在这里,如果键不存在,并发字典将代表我们执行该函数(否则它将运行该函数$updater),并且结果将被分配为条目值。

随后您可以像访问哈希表一样访问条目值:

$DBInventory[$vms[-1]] # returns array containing the query results from the last VM in the list
Run Code Online (Sandbox Code Playgroud)