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