In an event-driven script, I'm looking for a way to fire a custom event for the main thread from a background threadjob or runspace. The background treatment is time consuming and cannot be async, this is why it is delegated to a background threadjob or runspace. The main part of the script is event-driven to avoid periodic polling and unnecessary resource consumption.
The general structure of the script is as follows.
$DataCollectionScriptBlock = {
#
... Initialization stuff
#
while ( $stopRequested -ne 'yes' ) {
#
... collect / filter / consolidate data
#
fire event 'datacollection' for main Thread with necessary data as event argument
#
}
#
... termination stuff
#
}
$DataConsumerScriptBlock = {
# retrieve necessary data
$data = $Event.SourceEventArgs
#
... perform necessary stuff
#
}
#
# main thread operations
#
...
# start background job (or runspace)
Start-ThreadJob -ScriptBlock $DataCollectionScriptBlock -InputObject <something> ...
# subscibe to datacollection event
Register-EngineEvent -SourceIdentifier 'datacollection' -Action $DataConsumerScriptBlock
#
...
#
Wait-Event
To generate the event in $DataCollectionScriptBlock I tried the New-Event cmdlet, but it generates the event locally in the threadjob/runspace thread. The event cannot be used in the main thread.
I've also tried to use the BackgroundWorker class without success.
I've also read several posts on this forum with numerous examples in C# but without finding a simple solution to implement in Powershell.
Start-Job
uses cross-process parallelism, via a hidden child process; therefore, using events - which are an in-process feature - isn't supported.
However, via thread-based parallelism, where multiple runspaces run in parallel in the same process, a solution is possible, based on:
Identifying the target runspace that should receive an event, which in stand-alone PowerShell session is always the first runspace reported by Get-Runspace
((Get-Runspace)[0]
Calling the GenerateEvent()
method on the resulting System.Management.Automation.Runspaces.Runspace
instance's .Events
property.
For simplicity, the following self-contained example uses the PowerShell (Core) 7+'s -Parallel
feature of the ForEach-Object
cmdlet, which creates parallel runspaces and executes them synchronously.
However, I would expect the code to work equally with Start-ThreadJob
(ships with PowerShell v7+; installable on demand in Windows PowerShell) as well as with manually created runspaces via the PowerShell SDK.
#
# main thread operations
#
# Subscribe to a custom datacollection event
$job = Register-EngineEvent -SourceIdentifier datacollection -Action {
# Sample event processing that prints directly to the display.
@"
Event received:
Sender: $($Event.Sender)
Event args: $($Event.SourceArgs | Out-String)
"@ | Out-Host
}
# Use ForEach-Object -Parallel to create two parallel runspaces,
# and make them each trigger an event for the main runspace.
# I expect this to work equally with Start-ThreadJob and manual runspace creation.
1..2 | ForEach-Object -Parallel {
$null =
(Get-Runspace)[0].Events.GenerateEvent(
'datacollection', # event name
$_, # sender
@{ foo = 'bar' + $_ }, # event arguments
$null # extra data
)
}
# Keep the script alive until Ctrl-C is pressed
# (Due to use of an -Action script block with Register-EngineEvent,
# Wait-Process doesn't output any of the generated events.
# It just waits indefinitely, during which time the -Action script block can run).
try {
Wait-Event
} finally {
# Clean up.
$job | Remove-Job -Force
}
You should see the following display output, showing that both events were processed in the main runspace (ordering of the events isn't guaranteed and can vary):
Event received:
Sender: 2
Event args:
Name Value
---- -----
foo bar2
Event received:
Sender: 1
Event args:
Name Value
---- -----
foo bar1