Как передать пользовательскую функцию внутри ForEach-Object -Parallel

Я не могу найти способ передать функцию. Просто переменные.

Есть идеи без помещения функции в цикл ForEach?

function CustomFunction {
    Param (
        $A
    )
    Write-Host $A
}

$List = "Apple", "Banana", "Grape" 
$List | ForEach-Object -Parallel {
    Write-Host $using:CustomFunction $_
}

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

Другие вопросы по тегам