This example expands on the original question with explanation about code safety and performance.
The simplest approach is to pass the function as a string (this was answered correctly), however I feel the audience would benefit from understanding code safety implications of code injection in this way.
The example below uses powershell modules to provide a safer alternative and expands on the original correct answer showing how to also pass common parameters like '-Verbose'.
# Parallel Function scope-test.psm1
# demonstrates different ways to handle function scope in parallel runspaces
# reference: https://devblogs.microsoft.com/powershell/powershell-7-0-parallel-foreach-object/
# Various approaches are demonstrated including:
# 1. Failing approach where the function is not in scope
# 2. Common approach where the function is passed as a string and recreated in the parallel runspace
# 3. Safe approach where the module is imported in the parallel runspace
# 4. Clean approach where the module is imported using the 'using module' statement
# initialization
$module = $PSScriptRoot + '\scope-test.psm1'
function Start-Work {
# Function to be used in parallel runspaces
[CmdletBinding()]
Param([ValidateNotNullOrEmpty()][string] $message = 'starting work')
Write-Host $message
Write-Verbose "Work started at $(Get-Date -Format o)"
Start-Sleep -Seconds 1 # simulate work
}
# similar to the ToString() approach but is more explicit
$functionAsString = (Get-ChildItem function:Start-Work).Definition
function Test-ParallelFails {
# approach does not work because Start-Work function is out of scope in the parallel runspace
1..1 | ForEach-Object -Parallel {
Start-Work 'example that fails!'
}
}
function Test-ParallelByPassingFunctionAsString {
# Common recommended approach that recreates the required function in the parallel runspace
# using serialized script passed in a variable
# Unsafe because it allows for code injection if the variable is not controlled and
# is hard to validate for mitigation
1..9 | ForEach-Object -ThrottleLimit 3 -Parallel {
New-Item -Path Function:Start-Work -Value $using:functionAsString -Force | Out-Null
Start-Work "$_ : example where the function is passed using a string and recreated in the new runspace"
}
}
function Test-ParallelModule {
[CmdletBinding()] param ()
# Safe approach that imports the specified module into the parallel runspace,
# avoids code injection issues and verifies the module path.
# requires the module to be in a file or well-formed module folder
# This example also shows throttling at 3 threads and how to pass-through verbose
# preference (or other common parameters)
# This approach is also compatible with -AsJob if needed
1..9 | ForEach-Object -ThrottleLimit 3 -Parallel {
$modulePath = Resolve-Path $using:module
Import-Module $modulePath -Verbose:$using:VerbosePreference
Start-Work "$_ : example that uses import-module in the new runspace" -Verbose:$using:VerbosePreference
}
}
function Test-ParallelUsingModule {
# Clean approach that also works with classes, dotnet types and
# works by importing the module and any types into the parallel runspace
# requires the module to be in a file or well-formed module folder
# using module - has a requirement to be the first statement in the script block
# and this is why invoke-expression is used here
# Safer than recreating the function because input validation is used to mitigate code injection
# when using invoke-expression
# This approach is also compatible with -AsJob if needed
1..9 | ForEach-Object -ThrottleLimit 3 -Parallel {
$modulePath = Resolve-Path $using:module
Invoke-Expression "using module $modulePath"
Start-Work " $_ : example that uses 'using module' to import module, classes and types in the new runspace"
}
}
function Test-AllWithMeasureCommand {
# This function runs all 3 working approaches and measures the time taken for each
# tl;dr all approaches are similar in initialization performance and insignificant for most use cases
Measure-Command {
Test-ParallelByPassingFunctionAsString
}
Measure-Command {
Test-ParallelModule
}
Measure-Command {
Test-ParallelUsingModule
}
}
Export-ModuleMember -Function Test-ParallelFails, Test-ParallelByPassingFunctionAsString, Test-ParallelUsingModule, Test-ParallelModule, Test-AllWithMeasureCommand, Start-Work
-ParallelblockWrite-Hostis typically the wrong tool to use, unless the intent is to write to the display only, bypassing the success output stream and with it the ability to send output to other commands, capture it in a variable, redirect it to a file. To output a value, use it by itself; e.g.,$valueinstead ofWrite-Host $value(or useWrite-Output $value, though that is rarely needed). See also: the bottom section of stackoverflow.com/a/50416448/45375