提问人:Net Dawg 提问时间:10/29/2023 最后编辑:Net Dawg 更新时间:10/30/2023 访问量:167
PowerShell 使 ForEach 循环并行
PowerShell Make ForEach Loop Parallel
问:
这是工作代码:
$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 的参考,它也被考虑过,但没有太大进展。
答:
我不知道 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 开始。大括号需要在 -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
评论
以下内容应按预期工作(请参阅注释):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
-Process
foreach
foreach ($id in $ids) ...
由于 cmdlet 是一种命令类型,而不是语言语句(如 (或表达式)) - 它是在参数(分析)模式下分析的:
ForEach-Object
foreach
'foo'.Length
必须在一行上指定命令,但以下情况除外:
用行延续(将 a(所谓的反引号)放在行的最末尾)明确表示
`
)或者该行在语法上明确不完整,并强制 PowerShell 继续分析下一行命令的末尾。
在表达式(分析)模式下分析的语言语句(例如 和 )和表达式(例如 .NET 方法调用)通常不受此约束的约束。[2]
foreach
if
使用脚本块参数,可以使用语法不完整技术使命令多行:
- 仅将其开头
{
放在第一行,允许您将块的内容放在后续行上,如上所示。 - 请注意,脚本块的内容是一个新的解析上下文,其中上述规则再次适用。
- 仅将其开头
为了将操作应用于
$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
)
评论
回答我自己的问题,但这要归功于极简主义的@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。
评论
-Parallel
$ids | ForEach-Object -Parallel {