Есть ли более простой способ параллельного выполнения команд, сохраняя при этом эффективность в Windows PowerShell?

Этот самостоятельный ответ призван предоставить простую и эффективную альтернативу параллелизма для тех, кто застрял в Windows PowerShell и не может установить модули из-за, например, политик компании.

В Windows PowerShell доступны встроенные альтернативы для локальных параллельных вызовов.Start-Jobи, оба известны как очень медленные и неэффективные, и один из них (workflow) даже не рекомендуется использовать, и он больше не доступен в новых версиях PowerShell.

Другая альтернатива — полагаться на PowerShell SDK и кодировать собственную параллельную логику, используя то, чтоSystem.Management.Automation.RunspacesПространство имен может предложить. Это, безусловно, самый эффективный подход, и именноForEach-Object -Parallel(в PowerShell Core), а такжеStart-ThreadJob(предустановлен в PowerShell Core и доступен в Windows PowerShell через галерею PowerShell ).

Простой пример:

      $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

Это здорово, но становится утомительным и часто сложным, когда код более сложен и, как следствие, вызывает много вопросов.

Есть ли более простой способ сделать это?

2 ответа

Поскольку эта тема может сбивать с толку и часто вызывает вопросы на сайте, я решил создать эту функцию, которая может упростить эту утомительную задачу и помочь тем, кто застрял в Windows PowerShell. Цель состоит в том, чтобы сделать его как можно более простым и дружественным, это также должна быть функция, которую можно было бы скопировать и вставить в наш$PROFILEдля повторного использования при необходимости и не требует установки модуля (как указано в вопросе).

Эта функция была во многом вдохновлена ​​RamblingCookieMonster.Invoke-Parallelи Boe ProxPoshRSJobи представляет собой просто упрощенный вариант с некоторыми улучшениями.

ПРИМЕЧАНИЕ

Дальнейшие обновления этой функции будут публиковаться в официальном репозитории GitHub , а также в галерее PowerShell . Код в этом ответе больше не будет поддерживаться .

Вклады более чем приветствуются, если вы хотите внести свой вклад, разветвите репо и отправьте запрос на внесение изменений.


ОПРЕДЕЛЕНИЕ

      # 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')]
        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')]
            [Management.Automation.Runspaces.PSThreadOptions] $ThreadOptions = [Management.Automation.Runspaces.PSThreadOptions]::ReuseThread
        )

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

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

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

                $usingParams = @{}

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

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

                $pool  = [runspacefactory]::CreateRunspacePool(1, $ThrottleLimit, $iss, $Host)
                $tasks = [Collections.Generic.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.
                # Thanks to Jordan Borean for his help here.
                # https://github.com/jborean93
                if($usingParams.Count) {
                    $null = $ps.AddParameters(@{ '--%' = $usingParams })
                }

                $ps.RunspacePool = $pool

                $tasks.Add(@{
                    Instance    = $ps
                    AsyncResult = $ps.BeginInvoke()
                })
            }
            catch {
                $PSCmdlet.WriteError($_)
            }
        }
        end {
            try {
                while($tasks.Count) {
                    $id = [Threading.WaitHandle]::WaitAny($tasks.AsyncResult.AsyncWaitHandle, 200)

                    if($id -eq [Threading.WaitHandle]::WaitTimeout) {
                        continue
                    }

                    $task = $tasks[$id]
                    $task.Instance.EndInvoke($task.AsyncResult)

                    foreach($err in $task.Instance.Streams.Error) {
                        $PSCmdlet.WriteError($err)
                    }

                    $tasks.RemoveAt($id)
                }
            }
            catch {
                $PSCmdlet.WriteError($_)
            }
            finally {
                foreach($task in $tasks.Instance) {
                    if($task -is [IDisposable]) {
                        $task.Dispose()
                    }
                }

                if($pool -is [IDisposable]) {
                    $pool.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.1 и PowerShell 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: То же, что и предыдущий пример, но с параметром

      $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для передачи эталонного экземпляра в Runspaces

Этот метод рекомендуется при передаче эталонных экземпляров в пространства выполнения,$using:может не сработать в некоторых ситуациях.

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

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

$sync

ПРИМЕР 5: Демонстрирует, как передать локально определенную функцию в область выполнения.

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

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

ПАРАМЕТРЫ

-InputObject

Задает входные объекты, которые будут обрабатываться в ScriptBlock.
Примечание. Этот параметр предназначен для привязки к конвейеру.

      Type: Object
Parameter Sets: (All)
Aliases:

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

-ScriptBlock

Определяет операцию, выполняемую над каждым входным объектом.
Этот блок сценария запускается для каждого объекта в конвейере.

      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

-Переменные

Задает хэш-таблицу переменных, которые должны быть доступны в блоке сценария (пространствах выполнения). Ключи хеш-таблицы становятся именем переменной внутри блока сценария.

      Type: Hashtable
Parameter Sets: (All)
Aliases:

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

-Функции

Существующие функции в локальном сеансе должны быть доступны в блоке сценария (пространствах выполнения).

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

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

-ThreadOptions

Эти параметры определяют, будет ли создаваться новый поток при выполнении команды в среде выполнения.
Этот параметр ограничен иUseNewThread. Значение по умолчанию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 существует множество различных способов запуска сценариев для нескольких экземпляров, но большинство методов просто запускаются последовательно для одного сервера за раз.

Другие вопросы по тегам