Как передать пользовательскую функцию внутри ForEach-Object -Parallel
6 ответов
Решение не такое простое, как можно было бы надеяться:
function CustomFunction {
Param ($A)
"[$A]"
}
# Get the function's definition *as a string*
$funcDef = $function:CustomFunction.ToString()
"Apple", "Banana", "Grape" | ForEach-Object -Parallel {
# Define the function inside this thread...
$function:CustomFunction = $using:funcDef
# ... and call it.
CustomFunction $_
}
Такой подход необходим, потому что - помимо текущего местоположения (рабочего каталога) и переменных среды (которые применяются ко всему процессу) - потоки, которые
ForEach-Object -Parallel
create не видит состояние вызывающего, особенно в отношении переменных или функций (а также не пользовательские диски PS и импортированные модули).Что касается PowerShell 7.0, на GitHub обсуждается усовершенствование для поддержки копирования состояния вызывающего абонента в потоки по запросу, что сделает функции вызывающего абонента доступными.
Обратите внимание, что без доп. $funcDef
переменная и пытается переопределить функцию с помощью $function:CustomFunction = $using:function:CustomFunction
заманчиво, но $function:CustomFunction
это блок скрипта, а использование блоков скрипта с$using:
область действия явно запрещена.
$function:CustomFunction
является экземпляром нотации переменных пространства имен, который позволяет вам получить функцию (ее тело как[scriptblock]
instance) и установить (определить) его, назначив либо[scriptblock]
или строка, содержащая тело функции.
Я добавил целый набор пользовательских функций в параллельные процессы через файл ps1, используя включение внутри цикла. Благодаря этому вещи остаются очень чистыми и аккуратными.
ForEach-Object -Parallel {
# Include custom functions inside parallel scope
. $using:PSScriptRoot\CustomFunctions.ps1
# Now you can reference any function defined in the file
My-CustomFunction
....
Это действительно влечет за собой накладные расходы, требующие загрузки функций в каждом параллельном процессе, но в моем случае это было незначительно по сравнению с общим временем обработки.
Я просто придумал другой способ, используя get-команду, которая работает с оператором вызова. $a оказывается объектом FunctionInfo. РЕДАКТИРОВАТЬ: Мне сказали, что это не потокобезопасно, но я не понимаю, почему.
function hi { 'hi' }
$a = get-command hi
1..3 | foreach -parallel { & $using:a }
hi
hi
hi
Поэтому я придумал еще один маленький трюк, который может быть полезен для людей, пытающихся добавить функции динамически, особенно если вы можете заранее не знать их названия, например, когда функции находятся в массиве.
# Store the current function list in a variable
$initialFunctions=Get-ChildItem Function:
# Source all .ps1 files in the current folder and all subfolders
Get-ChildItem . -Recurse | Where-Object { $_.Name -like '*.ps1' } |
ForEach-Object { . "$($_.FullName)" }
# Get only the functions that were added above, and store them in an array
$functions = @()
Compare-Object $initialFunctions (Get-ChildItem Function:) -PassThru |
ForEach-Object { $functions = @($functions) + @($_) }
1..3 | ForEach-Object -Parallel {
# Pull the $functions array from the outer scope and set each function
# to its definition
$using:functions | ForEach-Object {
Set-Content "Function:$($_.Name)" -Value $_.Definition
}
# Call one of the functions in the sourced .ps1 files by name
SourcedFunction $_
}
Главный «трюк» в этом заключается в использовании
Set-Content
плюс имя функции, так как PowerShell по существу обрабатывает каждую запись
Function:
как путь.
Это имеет смысл, когда вы рассматриваете вывод
Get-PSDrive
. Поскольку каждая из этих записей может использоваться как «Диск» таким же образом (т.е. с двоеточием).
Это может быть более элегантный вариант с низким кодом для ввода функции вForeach-Object -Parallel
блокировать:
$m = New-Module -Name MyFunctions -ScriptBlock {
function Func-Timestamp {
return [DateTime]::Now.ToString("HH:mm:ss.ff")
}
}
$files | ForEach-Object -Parallel {
import-module $using:m -DisableNameChecking
[Console]::WriteLine("Here is the Time! $(Func-TimeStamp)")
}
Вы создаете$m
быть переменной модуля, а затем импортировать ее вForeach-Object
петля.
Если вы профессионал, конечно, вы добавили флаг специально, потому что вам действительно нужна параллельная обработка (поэтому см. принятый ответ)
Новички, вроде меня, могут подумать об удалении
-Parallel
флаг, потому что вы не поняли, что код, который вы скопировали откуда-то еще, на самом деле не нужен ... и тогда ваши вызовы функций работают как обычно.