Невозможно запустить задание с foreach-object параллельно

Я подготовил этот сценарий, чтобы попытаться выполнить параллельно одну и ту же функцию несколько раз с разными параметрами:

$myparams = "A", "B","C", "D"

$doPlan = {
    Param([string] $myparam)
        echo "print $myparam"
        # MakeARestCall is a function calling a web service
        MakeARestCall -myparam $myparam
        echo "done"
}

$myparams | Foreach-Object { 
    Start-Job -ScriptBlock $doPlan  -ArgumentList $_
}

Когда я запускаю его, вывод

Id     Name            PSJobTypeName   State         HasMoreData     Location             Command                  
--     ----            -------------   -----         -----------     --------             -------                  
79     Job79           BackgroundJob   Running       True            localhost            ...                      
81     Job81           BackgroundJob   Running       True            localhost            ...                      
83     Job83           BackgroundJob   Running       True            localhost            ...                      
85     Job85           BackgroundJob   Running       True            localhost            ...

но фактический вызов блока (а затем и веб-службы) не выполняется. Если я удалю объект foreach и заменю его обычным последовательным блоком foreach без Start-Job, веб-службы будут правильно вызваны. Это означает, что у меня проблема, когда я пытаюсь запустить блок параллельно.

Что я делаю неправильно?


person Manu    schedule 13.03.2020    source источник


Ответы (1)


Фоновые задания выполняются в независимых дочерних процессах, которые практически не разделяют состояние с вызывающим; конкретно:

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

  • Они не загружают (исходный код) ваши $PROFILE файлы, поэтому они не увидят никаких определений оттуда.

  • В PowerShell версии 6.x и ниже (включая Windows PowerShell) даже текущее расположение (каталог) не было унаследовано от вызывающего (по умолчанию [Environment]::GetFolderPath('MyDocuments')); это было исправлено в версии 7.0.

  • Единственный аспект состояния вызывающего сеанса, который они действительно видят, - это копии переменных среды вызывающего процесса.

  • Чтобы сделать значения переменных из сеанса вызывающего абонента доступными для фонового задания, на них необходимо ссылаться через $using:scope (см. _ 4_).

    • Note that with values other than strings, primitive types (such as numbers), and a handful of other well-known types, this can involve a loss of type fidelity, because the values are marshaled across process boundaries using PowerShell's XML-based serialization and deserialization; this potential loss of type fidelity also affects output from the job - see this answer for background information.
    • Использование гораздо более быстрых и менее ресурсоемких заданий thread через _ 5_, позволяет избежать этой проблемы (хотя действуют все остальные ограничения); Start-ThreadJob поставляется с PowerShell [Core] 6+ и может быть установлен по запросу в Windows PowerShell (например, Install-Module -Scope CurrentUser ThreadJob) - см. этот ответ для справочной информации.

Важно: всякий раз, когда вы используете задания для автоматизации, например, в сценарии, вызываемом из планировщика задач Windows или в контексте CI / CD, убедитесь, что вы дождитесь завершения всех заданий перед выходом из сценария (через _ 8_ или _ 9_), потому что сценарий, вызываемый через CLI полностью завершает процесс PowerShell, что уничтожает все незавершенные задания.

Следовательно, если только команда MakeARestCall:

  • оказывается файлом сценария (MakeARestCall.ps1) или исполняемым (MakeARestCall.exe), расположенным в одном из каталогов, перечисленных в $env:Path

  • оказывается функцией, определенной в модуле, который загружается автоматически,

ваш $doJob блок сценария завершится ошибкой при выполнении в процессе задания ', учитывая, что ни функция MakeARestCall, ни псевдоним не будут определены.

Ваши комментарии предполагают, что MakeARestCall действительно является функцией, поэтому для того, чтобы ваш код работал, вам придется (пере) определить функцию как часть выполняемого блока скрипта. по работе ($doJob, в вашем случае):

Следующий упрощенный пример демонстрирует эту технику:

# Sample function that simply echoes its argument.
function MakeARestCall { param($MyParam) "MakeARestCall: $MyParam" }

'foo', 'bar' | ForEach-Object {
  # Note: If Start-ThreadJob is available, use it instead of Start-Job,
  #       for much better performance and resource efficiency.
  Start-Job -ArgumentList $_ { 

    Param([string] $myparam)

    # Redefine the function via its definition in the caller's scope.
    # $function:MakeARestCall returns MakeARestCall's function body
    # which $using: retrieves from the caller's scope, assigning to
    # it defines the function in the job's scope.
    $function:MakeARestCall = $using:function:MakeARestCall

    # Call the recreated MakeARestCall function with the parameter.
    MakeARestCall -MyParam $myparam
  }
} | Receive-Job -Wait -AutoRemove

Вышеупомянутые выходные данные MakeARestCall: foo и MakeARestCall: bar демонстрируют, что (переопределенная) функция MakeARestCall была успешно вызвана в процессе задания.

Альтернативный подход:

Сделайте MakeARestCall скрипт (MakeARestCall.ps1) и на всякий случай вызовите его по его полному пути.

Например, если ваш скрипт находится в той же папке, что и вызывающий скрипт, вызывайте его как
& $using:PSScriptRoot\MakeARestCall.ps1 -MyParam $myParam

Конечно, если вы либо не против дублирования определения функции, либо только нуждаетесь в этом в контексте фоновых заданий, вы можете просто встроить определение функции непосредственно в блок скрипта.


Более простая и быстрая альтернатива PowerShell [Core] 7+, использующая ForEach-Object -Parallel:

Параметр -Parallel, представленный в _ 27_ в PowerShell 7 запускает указанный блок сценария в отдельном пространстве выполнения (потоке) для каждого входного объекта конвейера.

По сути, это более простой и удобный способ использования потоковых заданий (Start-ThreadJob) с такими же преимуществами производительности и использования ресурсов по сравнению с фоновыми заданиями, и с добавленной простотой прямого отчета о выходе потоков.

Однако отсутствие совместного использования состояния, описанное выше в отношении фоновых заданий, также применяется к потоковым заданиям (даже если они выполняются в тот же процесс, они делают это в изолированных пространствах выполнения PowerShell), поэтому здесь тоже функция MakARestCall должна быть (пере) определена (или встроена) в блок скрипта [1].

# Sample function that simply echoes its argument.
function MakeARestCall { param($MyParam) "MakeARestCall: $MyParam" }

# Get the function definition (body) *as a string*.
# This is necessary, because the ForEach-Object -Parallel explicitly
# disallows referencing *script block* values via $using:
$funcDef = $function:MakeARestCall.ToString()

'foo', 'bar' | ForEach-Object -Parallel {
  $function:MakeARestCall = $using:funcDef
  MakeARestCall -MyParam $_
}

Синтаксическая ошибка: -Parallel не является переключателем (параметр типа флага), но принимает блок скрипта для параллельного выполнения в качестве аргумента; другими словами: -Parallel должен быть помещен непосредственно перед блоком скрипта.

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

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

PS> 3, 1 | ForEach-Object -Parallel { Start-Sleep $_; "$_" }
1  # !! *Second* input's thread produced output *first*.
3

Чтобы отображать выходные данные в порядке ввода, что неизменно требует ожидания завершения всех потоков перед отображением вывода, вы можете добавить -AsJob переключатель:

  • Вместо прямого вывода затем возвращается простой (поточный) объект задания, который возвращает одно задание типа PSTaskJob, содержащее несколько дочерних jobs, по одному на каждое параллельное пространство выполнения (поток); вы можете управлять им с помощью обычных *-Job командлетов и получать доступ к отдельным дочерним заданиям через свойство .ChildJobs.

Дождавшись завершения всего задания, получая его выходные данные через _ 38_ затем показывает их в порядке ввода:

PS> 3, 1 | ForEach-Object -AsJob -Parallel { Start-Sleep $_; "$_" } |
      Receive-Job -Wait -AutoRemove
3  # OK, first input's output shown first, due to having waited.
1

[1] В качестве альтернативы, переопределите вашу MakeARestCall функцию как функцию фильтра (Filter), которая неявно работает с вводом конвейера через $_, чтобы вы могли использовать ее определение как ForEach-Object -Parallel скрипт блокировать как есть:

# Sample *filter* function that echoes the pipeline input it is given.
Filter MakeARestCall { "MakeARestCall: $_" }

# Pass the filter function's definition (which is a script block)
# directly to ForEach-Object -Parallel
'foo', 'bar' | ForEach-Object -Parallel $function:MakeARestCall
person mklement0    schedule 13.03.2020