powershelleventsbackground

How to fire a custom event for the main thread from a background threadjob or runspace


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.


Solution


  • 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:

    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].