PowerShell 使 ForEach 循环并行

PowerShell Make ForEach Loop Parallel

提问人:Net Dawg 提问时间:10/29/2023 最后编辑:Net Dawg 更新时间:10/30/2023 访问量:167

问:

这是工作代码:

$ids = 1..9 
$status  = [PSCustomObject[]]::new(10)
foreach ($id in $ids)
{ 
   $uriStr      = "http://192.168." + [String]$id + ".51/status"
   $uri         = [System.Uri] $uriStr
   $status[$id] = try {Invoke-RestMethod -Uri $uri -TimeOut 30}catch{}
}
$status

我想并行执行 ForEach 循环以探索性能改进。

我尝试的第一件事(结果很幼稚)是简单地引入 -parallel 参数

$ids = 1..9 
$status  = [PSCustomObject[]]::new(10)
foreach -parallel ($id in $ids)
{ 
   $uriStr      = "http://192.168." + [String]$id + ".51/status"
   $uri         = [System.Uri] $uriStr
   $status[$id] = try {Invoke-RestMethod -Uri $uri -TimeOut 30}catch{}
}
$status

这会导致以下错误,表明自 Powershell 7.3.9 起,此功能仍在考虑开发中:

ParserError: 
Line |
   3 |  foreach -parallel ($id in $ids)
     |  ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
     | The foreach '-parallel' parameter is reserved for future use.

我说幼稚是因为文档说并行参数仅在工作流中有效。但是,当我尝试时,我收到一个错误,指出不再支持工作流。

workflow helloworld {Write-Host "Hello World"}
ParserError: 
Line |
   1 |  workflow helloworld {Write-Host "Hello World"}
     |  ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
     | Workflow is not supported in PowerShell 6+.

然后我尝试了来自各种引用的各种组合(很好的例子),它建议 ForEach 与 ForEach-Object 根本不同,ForEach-Object 支持并行,如下所示(基本上是管道 ids):

$ids = 1..9 
$status  = [PSCustomObject[]]::new(10)
$ids | ForEach-Object -Parallel 
{ 
   $uriStr      = "http://192.168." + [String]$_ + ".51/status"
   $uri         = [System.Uri] $uriStr
   $status[$_] = try {Invoke-RestMethod -Uri $uri -TimeOut 30}catch{}
}
$status

这将生成以下错误:

ForEach-Object: 
Line |
   3 |  $ids | foreach-object -parallel
     |                        ~~~~~~~~~
     | Missing an argument for parameter 'Parallel'. Specify a parameter of type
     | 'System.Management.Automation.ScriptBlock' and try again.

   $uriStr      = "http://192.168." + [String]$_ + ".51/status"
   $uri         = [System.Uri] $uriStr
   $status[$i_] = try {Invoke-RestMethod -Uri $uri -TimeOut 30}catch{}

但是,在尝试了各种脚本块语义之后,这是我能做的最好的(基本上将 :using 应用于脚本块之外的状态变量):

$ids = 1..9 
$status  = [PSCustomObject[]]::new(10)
$myScriptBlock = 
{ 
   $uriStr      = "http://192.168." + [String]$_ + ".51/status"
   $uri         = [System.Uri] $uriStr
   {$using:status}[$_] = try {Invoke-RestMethod -Uri $uri -TimeOut 30}catch{}
}
$ids | foreach-object -parallel $myScriptBlock
$status

再次出现错误:无法索引到 Scriptblock 中

Line |
   4 |  … ng:status}[$_] = try {Invoke-RestMethod -Uri $uri -TimeOut 30}catch{}
     |                          ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
     | Unable to index into an object of type "System.Management.Automation.ScriptBlock".
InvalidOperation: 

还有其他几个值得一提的错误 - 如果不应用限定符,则得到错误:using

“无法索引到 null 数组”

这基本上意味着该变量在 foreach 或 script 块中无法识别。$status

所有其他表示限定符的方法都被拒绝,并出现以下错误:using

“赋值表达式无效” “使用 {}...”

因此,为了简洁起见,在问题陈述中更好地流动,因此被省略了。最后,这里是关于 Powershell 7.3+ 的 SciptBlocks 的参考,它也被考虑过,但没有太大进展。

PowerShell foreach 并行处理 脚本块 foreach-object

评论

3赞 Olaf 10/29/2023
在第三个代码示例中 - 将左大括号直接移到参数名称后面。😉-Parallel$ids | ForEach-Object -Parallel {
0赞 Net Dawg 10/29/2023
@Olaf - 谢谢。该建议确实将错误更改为“无法索引到空数组”,这基本上意味着变量$status在 foreach -parallel 循环块中无法识别,因此需要使用脚本块,这反过来又导致了引入的解决方案:using 如第四个代码示例中,以及相同的错误:“无法索引到 Scriptblock 类型的对象”。

答:

2赞 Hugo 10/29/2023 #1

我不知道 ForEach 的并行参数,但我知道您可以使用 Jobs 进行并行网络请求,您可以使用以下示例:

# Name of your jobs.
$JobName = "-StatusChecker"

# Holds the code we want to run in parallel.
$ScriptBlock = {
  param (
    $id
  )

  $uriStr      = "http://192.168." + [String]$id + ".51/status"
  $uri         = [System.Uri] $uriStr
  $response = try {
    Invoke-RestMethod -Uri $uri -TimeOut 30
  } catch {
    # Return a message for our results. Doesn't matter what you return but if it's null it will error.
    "Failed to grab status of ID: $id"
    # Write the error to the error stream but do not print it.
    Write-Error $_ -ErrorAction SilentlyContinue
  }

  # The $Error variable contains a list of errors that occurred during the run.
  # By returning it, we get the opportunity to revise what went wrong in the job.
  return $Error, $response, $id
}

# Grab all remaining jobs from the last time this script was run, stop and remove them.
# If you don't do this then it will mess up your results for each session as they aren't removed.
# We identify the relevant jobs by the JobName parameter set with Start-Job.
Get-Job -Name "*$JobName" | Stop-Job
Get-Job -Name "*$JobName" | Remove-Job


$ids = 1..9 
# Iterate through each id and create a job for each one.
foreach ($id in $ids) {

  # The job runs in parallel.
  Start-Job -ScriptBlock $ScriptBlock -ArgumentList @($id) -Name "ID-$ID-$JobName"
}

# Wait here until all jobs are complete.
$Jobs = Get-Job -Name "*$JobName" | Wait-Job

# Hold our results.
$status  = [PSCustomObject[]]::new(10)
# Grab the results of each job and format them into a nice table.
Foreach ($JobResult in $Jobs) {
  $Results = Receive-Job -Job $JobResult
  # $Results[0] is the error array returned by the job.
  # $Results[1] is $response from RestMethod.
  # $Results[2] is the $id.

  # Add returns to status list
  $Status[$Results[2]] = $Results[1]

  # Print each error found to the console.
  Foreach ($Err in $Results[0]) {
    Write-Error "Failed job for $($JobResult.Name). Error Message: $($Err)" -ErrorAction Continue
  }
}

# Final results.
$Status

您的代码位于 $ScriptBlock 变量中,下面的大部分代码都是关于从每个作业中检索结果并对其进行处理的。

评论

0赞 Net Dawg 10/30/2023
我只是想确认您的代码在我们的系统上有效!就在我几乎放弃并开始学习 python 的时候,Powershell 救赎了自己。我喜欢看似经典的方法。
0赞 Hugo 10/30/2023
@NetDawg很高兴它奏效了:)
2赞 js2010 10/29/2023 #2

这个例子对我有用。数组从 0 开始。大括号需要在 -parallel 之后在同一行上。

$ids = 0..9 
$status  = [PSCustomObject[]]::new(10)
$ids | foreach-object -parallel {
   $id = $_
   $mystatus = $using:status
   $mystatus[$id] = $id  # or  ($using:status) = $id
}
$status

0
1
2
3
4
5
6
7
8
9

或者,只需保存输出,而不必担心它是线程安全的:

$ids = 0..9 
$status = $ids | foreach-object -parallel {
  $id = $_
  $id
}
$status

评论

0赞 Net Dawg 10/30/2023
我喜欢这种回归基础的方法,这将是我的下一次尝试,以召唤类似的东西并在此基础上构建。但一直在想爱因斯坦,据说他说,“让事情尽可能简单,但不要更简单”(笑)......所以我想让我的代码尽可能粗糙(实际上我们有一个更复杂的情况),以便我理解这一切,尤其是适用的 Powershell 特质。但似乎对此的改编也可以正常工作。会尝试并报告 - 如果有问题!
2赞 mklement0 10/29/2023 #3

以下内容应按预期工作(请参阅注释):NOTE

$ids = 1..9 
$status  = [PSCustomObject[]]::new(10)
$ids | ForEach-Object -Parallel {  # NOTE: Opening { MUST be here.
   $uri = [System.Uri] "http://192.168.$_.51/status"
   # NOTE: (...) is required around the $using: reference.
   ($using:status)[$_] = try { Invoke-RestMethod -Uri $uri -TimeOut 30 } catch {}
}
$status

注意:由于 $_ 用作数组索引 ([$_]),因此 9 个输入 ID 的结果存储在数组元素中,从第二个 ID(索引为 1)开始,这意味着 $status[0] 将保持$null也许你的意思是使用 0..9

  • 你使用的是 PowerShell (Core) 7+,其中不再支持 PowerShell 工作流;因此,foreach 语句不支持此处。-parallel

  • 但是,PowerShell 7+ 支持作为 ForEach-Object cmdlet[1] 的参数进行多线程执行。-Parallel

    • 与不使用(即使用(通常位置绑定的)参数)一样,作为参数传递给 cmdlet 的脚本块 ({ ... } 不会在语句 () 中那样使用自选迭代器变量,而是从管道接收其输入,并使用自动 $_ 变量来引用手头的输入对象, 如上图所示。-Parallel-Processforeachforeach ($id in $ids) ...

    • 由于 cmdlet 是一种命令类型,而不是语言语句(如 (或表达式)) - 它是在参数(分析)模式下分析的:ForEach-Objectforeach'foo'.Length

      • 必须在一行上指定命令,但以下情况除外:

        • 用行延续(将 a(所谓的反引号)放在行的最末尾)明确表示`)

        • 或者该行在语法上明确不完整,并强制 PowerShell 继续分析下一行命令的末尾。

      • 表达式(分析)模式下分析的语言语句(例如 和 )和表达式(例如 .NET 方法调用)通常不受此约束的约束。[2]foreachif

      • 使用脚本块参数,可以使用语法不完整技术使命令多行:

        • 仅将其开头 { 放在第一行,允许您将块的内容放在后续行上,如上所示。
        • 请注意,脚本块的内容是一个新的解析上下文,其中上述规则再次适用。
  • 为了将操作应用于 $using:引用(从调用方的作用域访问变量的值),该引用设置索引 () 标识的属性或元素,或使用表达式获取属性值或使用非文本索引的元素,或方法调用,引用必须括在 (...)分组运算符[$_]$using:

    • 可以说,这应该不是必需的,但从 PowerShell 7.3.9 开始 - 请参阅 GitHub 问题 #10876 进行讨论。

    • 至于你的尝试:外壳创建了一个脚本块,这在这里没有意义;[3] 也许您的意思是分隔引用的标识符部分,在这种情况下,附件 : ;但是,这里不需要 (a) 和(b) 对问题没有帮助 - 无论哪种方式都需要整个参考。{$using:status}[$_]{...}$using:{...}$${using:status}(...)

  • 关于螺纹安全的注意事项:

    • 由于使用数组来存储结果,并且数组是固定大小的数据结构,并且使每个线程(运行空间)都成为数组的专用元素,因此无需显式管理并发访问。

    • 然而,更典型的是,对于可变大小的数据结构和/或在多个线程可能访问同一元素的情况下,管理并发性是必要的

    • 填充调用方提供的数据结构的替代方法是简单地使脚本块输出结果,调用方可以收集这些结果;但是,除非此输出还标识相应的输入对象,否则此对应关系将丢失。

    • 这个答案详细阐述了最后两点(线程安全数据结构与输出结果)。


[1] 有点令人困惑的是,ForEach-Object 有一个别名,也叫 foreach。在给定语句中,语法上下文分析模式)确定 foreach 是引用 foreach(语言)语句还是 ForEach-Object cmdlet;例如,foreach ($i in 1..3) { $i } (语句) vs. 1..3 | foreach { $_ } cmdlet)。

[2] 但是,如果表达式在给定行上的语法完整,PowerShell 也会停止解析,这相当于成员访问运算符 .的一个明显陷阱: 例如,与 C# 不同,. 必须与应用它的对象/表达式放在同一行上。例如,'foo'.<newline>长度有效,但 'foo'<newline> 。长度没有。此外,即使在一行上,. 也必须跟在目标对象/表达式后面(例如 'foo' .长度也中断)

[3] 由于 PowerShell 对类似列表的集合和标量(单个对象)的统一处理 - 请参阅此答案 - 索引到脚本块在技术上与获取值有关:索引 [0] 和 [-1] 返回标量本身(例如 $var = 42; $var[0]),所有其他索引默认返回$null,但如果 Set-StrictMode -Version 3 或更高版本生效,则会导致错误; 但是,尝试分配值绝对失败(例如,$var = 42;$var[0] = 43

评论

1赞 Net Dawg 10/30/2023
是的,先生!这正是我一直在寻找的解决方案。感谢知识学院。在我们的系统上进行了测试,它不仅有效,而且性能明显,当缩放时,往返时间从几分钟缩短到几秒钟!
0赞 Net Dawg 10/30/2023 #4

回答我自己的问题,但这要归功于极简主义的@js2010才华和@Hugo的惊人帖子(我将保留作为公认的答案),我什至可以理解这一点。

$ids    = 0..9 | Get-Random -Shuffle
$status = $ids | ForEach-Object -Parallel {
   $uri = [System.Uri] "http://192.168.$_.51/status"
   try {Invoke-RestMethod -Uri $uri -TimeOut 30}catch{}
} 
$status

同样,请先仔细阅读@Hugo,然后再@js2010。

评论

0赞 Net Dawg 10/30/2023
笔记。添加了随机化 id 列表,以避免 id 之间的任何优先级。假定/暗示的是 Restful Status 响应在其响应中包括 ids,或者更好的是包含完整的 uri,或两者兼而有之。否则,从REST调用返回的状态通常需要用id(当然)“丰富”,也许还需要其他有用的东西,如系统时间戳等。总而言之,任何现实的应用程序都需要以多种方式构建在这个基本的插图上,才能真正有用。这是留给读者定制的练习。提示 - 再读一遍@Hugo!