Невозможно начать работу с foreach-объектом параллельно - PullRequest
2 голосов
/ 13 марта 2020

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

$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 Ответ

2 голосов
/ 13 марта 2020

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

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

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

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

  • Аспект only состояния вызывающего сеанса, который они видят, являются копиями вызывающего процесса ' переменных среды .

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

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

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

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

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

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

* 11 07 * ваш $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 функция была успешно вызвана в процессе задания.

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

Make MakeARestCall script (MakeARestCall.ps1) и вызовите его по полному пути , чтобы быть в безопасности.

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

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


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

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

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

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

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

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

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

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

  • Вместо прямого вывода, * 1220 Затем возвращается одиночный, легкий (основанный на потоках) объект задания, который возвращает одиночное задание типа 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
Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...