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 PowerShell (Core) 7+'s -Parallel
feature of the ForEach-Object
cmdlet, which creates parallel runspaces and executes them synchronously.
However, the code works equally with Start-ThreadJob
(ships with PowerShell 7; 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.
$mainRs = [runspace]::DefaultRunspace
1..2 | ForEach-Object -Parallel {
$null =
($using:mainRs).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
}
Note:
Originally, the code above used (Get-Runspace)[0]
from inside the parallel runspaces to determine the main runspace. However, as RiverHeart discovered, this does not work when the code is run from the PIC (PowerShell Integrated Console),[1] i.e. the special shell provided by the PowerShell extension for Visual Studio Code.
Therefore, the code in the main runspace now obtains a reference to the latter via [runspace]::DefaultRunspace
and stores it in a variable, which the parallel threads can reference via the $using:
scope modifier.
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
[1] The PIC creates an extra, hidden runspace, and it is this extra runspace rather than the main one that is reported by (Get-Runspace)[0]
.