Невозможно запустить задание с 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