Powershell Foreach-Object -Parallel 如何更改循环外变量的值(跟踪进度)

Wha*_*m I 5 powershell foreach-object

此代码打印一个简单的进程,一次只做一件事:

$files = 1..100
$i = 0
$files | Foreach-Object {
    $progress = ("#" * $i)
    Write-Host  "`r$progress" -NoNewLine
    $i ++
    Start-Sleep -s 0.1
}
Run Code Online (Sandbox Code Playgroud)

但是,如果我想同时并行执行两件事,我无法输出进度,因为我无法更改并行循环之外的变量。这并没有完成所需的操作:

$files = 1..100
$i = 0
$files | Foreach-Object -ThrottleLimit 2 -Parallel {
    $progress = ("#" * $i)
    Write-Host  "`r$progress" -NoNewLine
    $i ++
    Start-Sleep -s 0.1
}
Run Code Online (Sandbox Code Playgroud)

我找不到一个好的解决方案来访问外部变量,不仅可以使用 $Using 读取它,还可以更改它。这在 Powershell 7 中可能吗?

mcl*_*ton 8

根据本文 - PowerShell ForEach-Object 并行功能- 您可以使用关键字引用“外部”脚本中的变量$using

例如

$files = 1..100
$i = 100;
$files | Foreach-Object -ThrottleLimit 2 -Parallel {
    write-host ($using:i)
    Start-Sleep -s .1
}
# 100
# 100
# etc

Run Code Online (Sandbox Code Playgroud)

但如果您尝试更新该值,您将得到以下信息:

$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.
Run Code Online (Sandbox Code Playgroud)

基本上,您不能分配变量$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
#
Run Code Online (Sandbox Code Playgroud)

它允许您更新值,但可能不是(可能不会)是线程安全的。


Fue*_*son 5

如果您正在使用较大的值-ThrottleLimit(例如 4+),则使用同步队列(为了线程安全)、Write-Progress和 jobs 可能是跟踪进度的一个很好的解决方案。正如其他人提到的,$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
    }
}
Run Code Online (Sandbox Code Playgroud)

根据我的经验,使用同步哈希表对于多个线程来说太混乱了;我想要一个单一、干净的进度条。但这取决于您的用例。我想我会将我的作品添加到其他优秀答案中。


js2*_*010 3

我认为这是正确的,基于使用 Foreach Parallel 跨多个线程编写进度它可能缺少一个锁,但仅对于写入进度而言,这可能不是什么大问题。在这种情况下,您也可以仅使用文件名来表示进度。

$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
}
Run Code Online (Sandbox Code Playgroud)

输出:

####################################################################################################
Run Code Online (Sandbox Code Playgroud)