Powershell Foreach-Object -Parallel 如何在循环外更改变量的值(跟踪进度)
Powershell Foreach-Object -Parallel how to change the value of a variable outside the loop (track progress)
此代码打印一个简单的进程,一次只做一件事:
$files = 1..100
$i = 0
$files | Foreach-Object {
$progress = ("#" * $i)
Write-Host "`r$progress" -NoNewLine
$i ++
Start-Sleep -s 0.1
}
但是如果我想同时并行做两件事,我不能输出进度,因为我不能在并行循环外改变变量。这不符合要求:
$files = 1..100
$i = 0
$files | Foreach-Object -ThrottleLimit 2 -Parallel {
$progress = ("#" * $i)
Write-Host "`r$progress" -NoNewLine
$i ++
Start-Sleep -s 0.1
}
我找不到一个好的解决方案来访问外部变量,不仅要用 $Using 读取它,还要更改它。这在 Powershell 7 中甚至可能吗?
根据这篇文章 - PowerShell ForEach-Object Parallel Feature - 您可以使用 $using
关键字从“外部”脚本引用变量:
例如
$files = 1..100
$i = 100;
$files | Foreach-Object -ThrottleLimit 2 -Parallel {
write-host ($using:i)
Start-Sleep -s .1
}
# 100
# 100
# etc
但是如果您尝试更新该值,您将得到:
$files | Foreach-Object -ThrottleLimit 2 -Parallel {
$using:i += $using:i
Start-Sleep -s .1
}
ParserError:
Line |
2 | $using:i += $using:i
| ~~~~~~~~
| The assignment expression is not valid. The input to an assignment operator must be an object that is able to accept
| assignments, such as a variable or a property.
基本上,您不能将 back 分配给 $using:i
变量。
您可以做的是改变复杂对象的属性 - 例如这个:
$counter = @{ "i" = 0 }
$files | Foreach-Object -ThrottleLimit 2 -Parallel {
($using:counter).i = ($using:counter).i + 1
Start-Sleep -s .1
}
$counter
# Name Value
# ---- -----
# i 100
#
它可以让您更新值,但可能不是(可能不会)是线程安全的。
基于Writing Progress across multiple threads with Foreach Parallel. It may be missing a lock,我认为这是对的,但对于写作进度来说,这可能不是什么大问题。在这种情况下,您也可以只使用文件名来表示进度。
$hash = @{i = 1}
$sync = [System.Collections.Hashtable]::Synchronized($hash)
$files = 1..100
$files | Foreach-Object -throttlelimit 2 -parallel {
$syncCopy = $using:sync
$progress = '#' * $syncCopy.i
#$progress = '#' * $_
Write-Host "`r$progress" -NoNewLine
$syncCopy.i++
Start-Sleep .1
}
输出:
####################################################################################################
如果您使用更大的 -ThrottleLimit
值(比如 4+),使用同步队列(为了线程安全),Write-Progress
,作业可以是跟踪进度的一个很好的解决方案.正如其他人提到的,$Using
关键字允许您访问脚本块范围内的变量:
$files = 1..100
$queue = [System.Collections.Queue]::new()
1..$files.Count | ForEach-Object { $queue.Enqueue($_) }
$syncQueue = [System.Collections.Queue]::synchronized($queue)
$job = $files | ForEach-Object -AsJob -ThrottleLimit 6 -Parallel {
$sqCopy = $Using:syncQueue
#Simulating work...do stuff with files here
Start-Sleep (Get-Random -Maximum 10 -Minimum 1)
#Dequeue element to update progress
$sqCopy.Dequeue()
}
#While $job is running, update progress bar
while ($job.State -eq 'Running') {
if ($syncQueue.Count -gt 0) {
$status = ((1 / $syncQueue.Count) * 100)
Write-Progress -Activity "Operating on Files" -Status "Status" -PercentComplete $status
Start-Sleep -Milliseconds 100
}
}
根据我的经验,使用同步哈希表对于多个线程来说太混乱了;我想要一个单一的、干净的进度条。不过,这取决于您的用例。以为我会把我的文章添加到其他优秀的答案中。
此代码打印一个简单的进程,一次只做一件事:
$files = 1..100
$i = 0
$files | Foreach-Object {
$progress = ("#" * $i)
Write-Host "`r$progress" -NoNewLine
$i ++
Start-Sleep -s 0.1
}
但是如果我想同时并行做两件事,我不能输出进度,因为我不能在并行循环外改变变量。这不符合要求:
$files = 1..100
$i = 0
$files | Foreach-Object -ThrottleLimit 2 -Parallel {
$progress = ("#" * $i)
Write-Host "`r$progress" -NoNewLine
$i ++
Start-Sleep -s 0.1
}
我找不到一个好的解决方案来访问外部变量,不仅要用 $Using 读取它,还要更改它。这在 Powershell 7 中甚至可能吗?
根据这篇文章 - PowerShell ForEach-Object Parallel Feature - 您可以使用 $using
关键字从“外部”脚本引用变量:
例如
$files = 1..100
$i = 100;
$files | Foreach-Object -ThrottleLimit 2 -Parallel {
write-host ($using:i)
Start-Sleep -s .1
}
# 100
# 100
# etc
但是如果您尝试更新该值,您将得到:
$files | Foreach-Object -ThrottleLimit 2 -Parallel {
$using:i += $using:i
Start-Sleep -s .1
}
ParserError:
Line |
2 | $using:i += $using:i
| ~~~~~~~~
| The assignment expression is not valid. The input to an assignment operator must be an object that is able to accept
| assignments, such as a variable or a property.
基本上,您不能将 back 分配给 $using:i
变量。
您可以做的是改变复杂对象的属性 - 例如这个:
$counter = @{ "i" = 0 }
$files | Foreach-Object -ThrottleLimit 2 -Parallel {
($using:counter).i = ($using:counter).i + 1
Start-Sleep -s .1
}
$counter
# Name Value
# ---- -----
# i 100
#
它可以让您更新值,但可能不是(可能不会)是线程安全的。
基于Writing Progress across multiple threads with Foreach Parallel. It may be missing a lock,我认为这是对的,但对于写作进度来说,这可能不是什么大问题。在这种情况下,您也可以只使用文件名来表示进度。
$hash = @{i = 1}
$sync = [System.Collections.Hashtable]::Synchronized($hash)
$files = 1..100
$files | Foreach-Object -throttlelimit 2 -parallel {
$syncCopy = $using:sync
$progress = '#' * $syncCopy.i
#$progress = '#' * $_
Write-Host "`r$progress" -NoNewLine
$syncCopy.i++
Start-Sleep .1
}
输出:
####################################################################################################
如果您使用更大的 -ThrottleLimit
值(比如 4+),使用同步队列(为了线程安全),Write-Progress
,作业可以是跟踪进度的一个很好的解决方案.正如其他人提到的,$Using
关键字允许您访问脚本块范围内的变量:
$files = 1..100
$queue = [System.Collections.Queue]::new()
1..$files.Count | ForEach-Object { $queue.Enqueue($_) }
$syncQueue = [System.Collections.Queue]::synchronized($queue)
$job = $files | ForEach-Object -AsJob -ThrottleLimit 6 -Parallel {
$sqCopy = $Using:syncQueue
#Simulating work...do stuff with files here
Start-Sleep (Get-Random -Maximum 10 -Minimum 1)
#Dequeue element to update progress
$sqCopy.Dequeue()
}
#While $job is running, update progress bar
while ($job.State -eq 'Running') {
if ($syncQueue.Count -gt 0) {
$status = ((1 / $syncQueue.Count) * 100)
Write-Progress -Activity "Operating on Files" -Status "Status" -PercentComplete $status
Start-Sleep -Milliseconds 100
}
}
根据我的经验,使用同步哈希表对于多个线程来说太混乱了;我想要一个单一的、干净的进度条。不过,这取决于您的用例。以为我会把我的文章添加到其他优秀的答案中。