You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何改进PowerShell v5中ForEach-Parallel函数的实现?

PowerShell v5 并行处理函数问题与改进

问题背景

我发现了一个适用于PowerShell v5的ForEach-Parallel函数,正尝试对其进行改进。由于工作依赖的代码库仅支持v5版本,无法升级到PowerShell v7.x。

遇到的问题

  • 并行网络命令效率低下:运行网络相关命令时,并行执行的时间节省效果微乎其微,怀疑是否受限于Windows 10的TCP栈?
  • 示例运行报错:第二个使用Test-NetConnection的示例,运行前会输出错误:
    One or more errors occurred.
    
  • 参数设计不合理:当前实现仅通过假设-ScriptBlock参数处于位置0来隐藏该参数,这种方式不够严谨。

当前函数代码

function Invoke-ForEachParallel {
    [CmdletBinding()]
    [Alias('ForEach-Parallel')]
    [Diagnostics.CodeAnalysis.SuppressMessageAttribute(
        "PSAvoidUsingAliases",
        '',
        Justification = 'The used verb makes the most sense in this case.'
    )]
    param (
        [Parameter(Mandatory, ValueFromPipeline)]
        [Array]$Items, #! 代码中未直接使用该参数,仅支持管道输入

        [Parameter(Mandatory, Position = 0)]
        [Alias ('Process')] #* 为兼容foreach语法的不合理实现方式
        [scriptblock]$ScriptBlock,

        [Parameter()]
        [Object[]]$ArgumentList,

        [Parameter()]
        [int]$MaxRunspaces = 16,

        [Parameter()]
        [int]$TimeOut = (60 * 60 * 1000) #TODO: 是否需要支持秒作为输入单位?
    )

    if ($Input) {
        try {
            # 创建用于脚本块的可选参数列表
            $Arguments  = ''
            $Parameters = ''

            if ($ArgumentList) {
                for ($Index = 0; $Index -lt $ArgumentList.Length; $Index++) {
                    $Arguments  += ", `$$Index"
                    $Parameters += " `$$Index"
                }
            }

            # 创建要执行的脚本块
            # - 包裹传入的脚本块,以便将参数(ArgumentList)和当前项($_)一起传递
            # - 始终加载当前模块

            $ScriptText =
@"
[CmdletBinding()]
param (`$_$Arguments)

function Wrapper {
$ScriptBlock
}

Wrapper $Parameters
"@

            $SessionState = [System.Management.Automation.Runspaces.InitialSessionState]::CreateDefault()

            # 传入当前主机,让每个运行空间的输出显示在当前主机中
            $Pool = [runspacefactory]::CreateRunspacePool(1, $MaxRunspaces, $sessionState, $Host)
            $Pool.Open()

            # 为每个项创建新的运行空间
            $Runspaces      = @()
            $AsyncResults   = @()
            $Exceptions     = @()

            foreach ($Item in $Input) {
                $Runspace = [powershell]::create()
                $Runspace.RunSpacePool = $Pool
                $Runspaces += $Runspace

                $Runspace.Streams.Error.add_DataAdded({
                    Param (
                        [Object]$Sender,
                        [System.Management.Automation.DataAddedEventArgs]$e #!$e 用途未明确标注
                    )

                    foreach ($Item in $Sender.ReadAll()) {
                        throw "$($Item.Exception.Message)"
                    }
                })

                # 添加生成的脚本块,传入当前项和可选参数
                [void]$Runspace.AddScript($ScriptText)
                [void]$Runspace.AddArgument($Item)

                if ($ArgumentList) {
                    [void]$Runspace.AddParameters($ArgumentList)
                }

                # 传递Verbose参数
                [void]$Runspace.AddParameter('Verbose', $VerbosePreference -eq 'Continue')

                # 同步启动运行空间
                $AsyncResult    = $Runspace.BeginInvoke()
                $AsyncResults   += $AsyncResult

            }#end foreach

            # 等待所有运行空间完成
            for ($Index = 0; $Index -lt $AsyncResults.Length; $Index++) {
                $null = [System.Threading.WaitHandle]::WaitAll($AsyncResults[$Index].AsyncWaitHandle, $TimeOut)
            }

            # 获取每个运行空间的结果
            $Errors = @()

            for ($Index = 0; $Index -lt $AsyncResults.Length; $Index++) {
                $AsyncResult    = $AsyncResults[$Index]
                $Runspace       = $Runspaces[$Index]

                # 如有需要,以下属性可提供运行空间完成状态的详细信息
                # $runspace.InvocationStateInfo.State
                # $runspace.InvocationStateInfo.Reason

                try {
                    Write-Output ($Runspace.EndInvoke($AsyncResult))
                }
                catch {
                    # 收集所有错误,统一返回
                    $Errors += $_
                }

            }#end for

        }#end try

        finally {
            if ($Pool) {
                $Pool.Close()
            }
        }

        # 处理错误
        if ($Errors) {
            if ($Errors.Length -eq 1) {
                throw $Errors[0]
            }
            else {
                $Exceptions = [exception[]]($Errors).Exception
                $AllMessages = $([string]::Join("`n", $Exceptions.Message))
                throw (New-Object AggregateException -ArgumentList "发生一个或多个错误:`n$AllMessages",$Exceptions)
            }
        }

    }#endif Input

<#
.SYNOPSIS
    使用PowerShell运行空间实现并行foreach。

.DESCRIPTION
    使用PowerShell运行空间实现并行foreach。

.NOTES
    CodeblackNL的MIT许可证

.LINK
    https://github.com/CodeblackNL/chocolatey-oneget-provider/tree/master

.PARAMETER Items
    要为其执行脚本块的项。

.PARAMETER ScriptBlock
    要为每个项执行的脚本块。

.PARAMETER ArgumentList
    要传递给脚本块的对象参数数组。

.PARAMETER MaxRunspaces
    尝试并行运行的最大运行空间数。
    实际并行执行的运行空间数由运行时决定,例如受可用核心数限制。
    默认值为16。

.PARAMETER TimeOut
    等待每个运行空间完成的时间,单位为毫秒。
    默认值为1小时。

.EXAMPLE
    1..10 | foreach {Invoke-WebRequest -Uri www.bing.com | select StatusCode}

    1..10 | foreach-parallel {Invoke-WebRequest -Uri www.bing.com | select StatusCode}

    并行10次获取网站的状态码。

.EXAMPLE
    1..10 | foreach {Test-NetConnection www.bing.com -Port 443 | select TcpTestSucceeded}

    1..10 | foreach-parallel {Test-NetConnection www.bing.com -Port 443 | select TcpTestSucceeded}

    并行10次测试TCP端口是否可打开。

.EXAMPLE
    (1..10) | foreach {'Sleeping'; sleep -Seconds 1}

    (1..10) | foreach-parallel {'Sleeping'; sleep -Seconds 1}

    并行10次休眠1秒。

#>

}

内容的提问来源于stack exchange,提问作者Dennis

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.14 21:55:01