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
    }
}

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