Невозможно запустить задание с 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, веб-службы будут вызваны правильно. Это означает, что у меня проблема, когда я пытаюсь запустить блок параллельно.

Что я делаю не так?

1 ответ

Решение

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

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

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

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

  • Только аспект состояния вызывающего сеанса они видят являются копиями вызывающего процесса переменных окружением.

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

    • Обратите внимание, что со значениями, отличными от строк, примитивными типами (такими как числа) и несколькими другими хорошо известными типами, это может привести к потере точности типа, поскольку значения маршалируются через границы процесса с использованием сериализации PowerShell на основе XML и десериализация; эта потенциальная потеря верности типа также влияет на результат работы - см. этот ответ для справочной информации.
    • Использование гораздо более быстрых и менее ресурсоемких потоковых заданий с помощью Start-ThreadJob, позволяет избежать этой проблемы (хотя действуют все остальные ограничения); Start-ThreadJob поставляется с PowerShell [Core] 6+ и может быть установлен по запросу в Windows PowerShell (например, Install-Module -Scope CurrentUser ThreadJob) - см. этот ответ для получения дополнительной информации.

Важно: всякий раз, когда вы используете задания для автоматизации, например, в сценарии, вызываемом из планировщика задач Windows или в контексте CI / CD, обязательно дождитесь завершения всех заданий перед выходом из сценария (через Receive-Job -Wait или Wait-Job), потому что сценарий, вызываемый через интерфейс командной строки PowerShell, завершает процесс 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 параметр, введенный в ForEach-Object в 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состоит из нескольких дочерних заданий, по одному для каждого параллельного пространства выполнения (потока); вы можете справиться с этим обычным*-Job командлеты и доступ к отдельным дочерним заданиям через .ChildJobs свойство.

Ожидая за общую работу на полный, получая свои выходы через Receive-Job затем показывает их в порядке ввода:

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
Другие вопросы по тегам