簡體   English   中英

在 Windows PowerShell 中,有沒有更簡單的方法來並行運行命令,同時保持高效?

[英]Is there an easier way to run commands in parallel while keeping it efficient in Windows PowerShell?

這個自我回答旨在為那些堅持 Windows PowerShell 並且由於公司政策等原因無法安裝模塊的人提供一種簡單有效的並行性替代方案。

在 Windows PowerShell 中,本地並行調用的內置可用替代方案是Start-Jobworkflow ,兩者都非常緩慢,效率低下,甚至不建議使用其中之一( workflow )並且在新版本中不再可用PowerShell

另一種選擇是依賴PowerShell SDK並使用System.Management.Automation.Runspaces命名空間必須提供的內容編寫我們自己的並行邏輯。 這絕對是最有效的方法,並且是ForEach-Object -Parallel (在 PowerShell Core 中)以及Start-ThreadJob (預裝在 PowerShell Core 中,在 Windows PowerShell 中通過PowerShell Gallery可用)在幕后使用的方法。

一個簡單的例子:

$throttlelimit = 3

$pool = [runspacefactory]::CreateRunspacePool(1, $throttlelimit)
$pool.Open()

$tasks = 0..10 | ForEach-Object {
    $ps = [powershell]::Create().AddScript({
        'hello world from {0}' -f [runspace]::DefaultRunspace.InstanceId
        Start-Sleep 3
    })
    $ps.RunspacePool = $pool

    @{ Instance = $ps; AsyncResult = $ps.BeginInvoke() }
}

$tasks | ForEach-Object {
    $_.Instance.EndInvoke($_.AsyncResult)
}

$tasks.Instance, $pool | ForEach-Object Dispose

這很好,但是當代碼更復雜並因此帶來很多問題時,它會變得乏味且常常變得復雜。

有更簡單的方法嗎?

由於這是一個可能令人困惑並且經常給網站帶來問題的主題,我決定創建這個 function 可以簡化這項繁瑣的任務並幫助那些陷入困境的人 Windows PowerShell。目的是讓它盡可能簡單和友好,它也應該是一個 function,可以復制粘貼到我們的$PROFILE中,以便在需要時重復使用,而不需要安裝模塊(如問題中所述)。

這個 function 受到 RamblingCookieMonster 的Invoke-Parallel和 Boe Prox 的PoshRSJob的極大啟發,只是對那些進行了一些改進的簡化版本。

筆記

此 function 的進一步更新將發布到官方GitHub 回購以及PowerShell 畫廊 此答案中的代碼將不再維護

非常歡迎貢獻,如果你想貢獻,fork 回購並提交帶有更改的拉取請求。


定義

using namespace System.Collections.Generic
using namespace System.Management.Automation
using namespace System.Management.Automation.Runspaces
using namespace System.Management.Automation.Language
using namespace System.Text

# The function must run in the scope of a Module.
# `New-Module` must be used for portability. Otherwise store the
# function in a `.psm1` and import it via `Import-Module`.

New-Module PSParallelPipeline -ScriptBlock {
    function Invoke-Parallel {
        [CmdletBinding(PositionalBinding = $false)]
        [Alias('parallel', 'parallelpipeline')]
        param(
            [Parameter(Mandatory, ValueFromPipeline)]
            [object] $InputObject,

            [Parameter(Mandatory, Position = 0)]
            [scriptblock] $ScriptBlock,

            [Parameter()]
            [int] $ThrottleLimit = 5,

            [Parameter()]
            [hashtable] $Variables,

            [Parameter()]
            [ArgumentCompleter({
                param(
                    [string] $commandName,
                    [string] $parameterName,
                    [string] $wordToComplete
                )

                (Get-Command -CommandType Filter, Function).Name -like "$wordToComplete*"
            })]
            [string[]] $Functions,

            [Parameter()]
            [ValidateSet('ReuseThread', 'UseNewThread')]
            [PSThreadOptions] $ThreadOptions = [PSThreadOptions]::ReuseThread
        )

        begin {
            try {
                $iss = [initialsessionstate]::CreateDefault2()

                foreach($key in $Variables.PSBase.Keys) {
                    $iss.Variables.Add([SessionStateVariableEntry]::new($key, $Variables[$key], ''))
                }

                foreach($function in $Functions) {
                    $def = (Get-Command $function).Definition
                    $iss.Commands.Add([SessionStateFunctionEntry]::new($function, $def))
                }

                $usingParams = @{}

                foreach($usingstatement in $ScriptBlock.Ast.FindAll({ $args[0] -is [UsingExpressionAst] }, $true)) {
                    $varText = $usingstatement.Extent.Text
                    $varPath = $usingstatement.SubExpression.VariablePath.UserPath

                    # Credits to mklement0 for catching up a bug here. Thank you!
                    # https://github.com/mklement0
                    $key = [Convert]::ToBase64String([Encoding]::Unicode.GetBytes($varText.ToLower()))
                    if(-not $usingParams.ContainsKey($key)) {
                        $usingParams.Add($key, $PSCmdlet.SessionState.PSVariable.GetValue($varPath))
                    }
                }

                $pool  = [runspacefactory]::CreateRunspacePool(1, $ThrottleLimit, $iss, $Host)
                $tasks = [List[hashtable]]::new()
                $pool.ThreadOptions = $ThreadOptions
                $pool.Open()
            }
            catch {
                $PSCmdlet.ThrowTerminatingError($_)
            }
        }
        process {
            try {
                # Thanks to Patrick Meinecke for his help here.
                # https://github.com/SeeminglyScience/
                $ps = [powershell]::Create().AddScript({
                    $args[0].InvokeWithContext($null, [psvariable]::new('_', $args[1]))
                }).AddArgument($ScriptBlock.Ast.GetScriptBlock()).AddArgument($InputObject)

                # This is how `Start-Job` does it's magic. Credits to Jordan Borean for his help here.
                # https://github.com/jborean93

                # Reference in the source code:
                # https://github.com/PowerShell/PowerShell/blob/7dc4587014bfa22919c933607bf564f0ba53db2e/src/System.Management.Automation/engine/ParameterBinderController.cs#L647-L653
                if($usingParams.Count) {
                    $null = $ps.AddParameters(@{ '--%' = $usingParams })
                }

                $ps.RunspacePool = $pool

                $tasks.Add(@{
                    Instance    = $ps
                    AsyncResult = $ps.BeginInvoke()
                })
            }
            catch {
                $PSCmdlet.WriteError($_)
            }
        }
        end {
            try {
                foreach($task in $tasks) {
                    $task['Instance'].EndInvoke($task['AsyncResult'])

                    if($task['Instance'].HadErrors) {
                        $task['Instance'].Streams.Error
                    }
                }
            }
            catch {
                $PSCmdlet.WriteError($_)
            }
            finally {
                $tasks.Instance, $pool | ForEach-Object Dispose
            }
        }
    }
} -Function Invoke-Parallel | Import-Module -Force

句法

Invoke-Parallel -InputObject <Object> [-ScriptBlock] <ScriptBlock> [-ThrottleLimit <Int32>]
 [-ArgumentList <Hashtable>] [-ThreadOptions <PSThreadOptions>] [-Functions <String[]>] [<CommonParameters>]

要求

Windows PowerShell 5.1PowerShell Core 7+兼容。

安裝

如果您希望通過圖庫安裝它並將其作為模塊提供:

Install-Module PSParallelPipeline -Scope CurrentUser

例子

示例 1:並行批處理運行緩慢的腳本

$message = 'Hello world from {0}'

0..10 | Invoke-Parallel {
    $using:message -f [runspace]::DefaultRunspace.InstanceId
    Start-Sleep 3
} -ThrottleLimit 3

示例 2:與前面的示例相同,但帶有-Variables參數

$message = 'Hello world from {0}'

0..10 | Invoke-Parallel {
    $message -f [runspace]::DefaultRunspace.InstanceId
    Start-Sleep 3
} -Variables @{ message = $message } -ThrottleLimit 3

示例 3:添加到單線程安全實例

$sync = [hashtable]::Synchronized(@{})

Get-Process | Invoke-Parallel {
    $sync = $using:sync
    $sync[$_.Name] += @( $_ )
}

$sync

示例 4:與前面的示例相同,但使用-Variables將引用實例傳遞給運行空間

在將引用實例傳遞到運行空間時,建議使用此方法, $using:在某些情況下可能會失敗。

$sync = [hashtable]::Synchronized(@{})

Get-Process | Invoke-Parallel {
    $sync[$_.Name] += @( $_ )
} -Variables @{ sync = $sync }

$sync

示例 5:演示如何將本地定義的 Function 傳遞給運行空間 scope

function Greet { param($s) "$s hey there!" }

0..10 | Invoke-Parallel {
    Greet $_
} -Functions Greet

參數

-輸入對象

指定要在 ScriptBlock 中處理的輸入對象。
注意:此參數旨在從管道綁定。

Type: Object
Parameter Sets: (All)
Aliases:

Required: True
Position: Named
Default value: None
Accept pipeline input: True (ByValue)
Accept wildcard characters: False

-腳本塊

指定對每個輸入 object 執行的操作。
此腳本塊針對管道中的每個 object 運行。

Type: ScriptBlock
Parameter Sets: (All)
Aliases:

Required: True
Position: 1
Default value: None
Accept pipeline input: False
Accept wildcard characters: False

-油門限制

指定並行調用的腳本塊數。
輸入對象將被阻止,直到運行的腳本阻止計數低於 ThrottleLimit。
默認值為5

Type: Int32
Parameter Sets: (All)
Aliases:

Required: False
Position: Named
Default value: 5
Accept pipeline input: False
Accept wildcard characters: False

-變量

指定在腳本塊(運行空間)中可用的 hash 變量表。 hash 表鍵成為腳本塊內的變量名。

Type: Hashtable
Parameter Sets: (All)
Aliases:

Required: False
Position: Named
Default value: None
Accept pipeline input: False
Accept wildcard characters: False

-職能

本地 Session 中的現有函數在腳本塊(運行空間)中可用。

Type: String[]
Parameter Sets: (All)
Aliases:

Required: False
Position: Named
Default value: None
Accept pipeline input: False
Accept wildcard characters: False

-線程選項

這些選項控制在運行空間中執行命令時是否創建新線程。
此參數僅限於ReuseThreadUseNewThread 默認值為ReuseThread
有關詳細信息,請參閱PSThreadOptions枚舉

Type: PSThreadOptions
Parameter Sets: (All)
Aliases:
Accepted values: Default, UseNewThread, ReuseThread, UseCurrentThread

Required: False
Position: Named
Default value: ReuseThread
Accept pipeline input: False
Accept wildcard characters: False

PowerShell 工作流提供了一種針對多個服務器並行運行 PowerShell 模塊和腳本的強大方法。 PowerShell 中有很多不同的方法可以針對多個實例運行腳本,但大多數方法只是一次連續運行一台服務器。

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM