EventHandler and asynchrony

Spread the love

Events in .NET are for the most part inherently synchronous multicast delegates. It is therefore no surprise that async and traditional EventHandler-based delegates do not mix. Today I’ll illustrate one design approach which I’ve used successfully to get around this incompatibility.

The scenario (slightly simplified for explanatory purposes) was a looping scheduler of background work items. The scheduler had various lifecycle events such as Draining (“waiting for pending work items to complete”), Paused (“all further work is on hold for now”), Resuming (“preparing to start work items again”), and so on. This scheduler was built to be purely async and as such its inner loop expected to interact only with async-friendly (i.e. non-blocking) code. This of course presented a challenge since the lifecycle events were implemented as good old EventHandler<TEventArgs> derivatives.

One way to “solve” this is by restriction — i.e. no async code allowed for event subscribers. I don’t mean to denigrate this approach because this is actually a very reasonable strategy which could simplify the design. However, in the scenario I’m describing there were at least a few valid use cases which could be concisely implemented by making some calls to a remote server. There surely would have been viable workarounds, perhaps by implementing such logic as a work item itself to be scheduled. But this would have been less straightforward and possibly encourage users of the library to take the relatively easier (but arguably wrong) path of just writing blocking sync code directly in the handler.

Instead, I got some inspiration from CancelEventArgs and the way it allows the user to set the Cancel property to relay information back to the event sender. With that in mind, I created this base class AsyncEventArgs to maintain a list of async tasks to be scheduled after all handlers were done:

public class AsyncEventArgs : EventArgs
{
    public AsyncEventArgs()
    {
        this.Tasks = new List<Func<Task>>();
    }

    public IList<Func<Task>> Tasks { get; private set; }
}

Here is a sample use case which invokes a world clock REST API to decide whether to stop:

DateTime initialTime = await worldClock.GetTimeAsync();
Log("Time is " + TimeString(initialTime));

// An infinite loop of successive 100 ms delay tasks
Func<Task> delayAsync = delegate
{
    Log("Delaying...");
    return Task.Delay(TimeSpan.FromSeconds(0.1d));
};

LoopingScheduler scheduler = new LoopingScheduler(delayAsync);

Func<DateTime, TimeSpan, Task> throwIfMaxDurationAsync = async delegate(DateTime start, TimeSpan duration)
{
    Log("Checking time...");
    DateTime now = await worldClock.GetTimeAsync();
    Log("Time is " + TimeString(now));
    if ((now - start) > duration)
    {
        throw new InvalidOperationException("Max duration reached!");
    }
};

// On every pause cycle, check if duration is reached based on world time
scheduler.Paused += delegate(object sender, AsyncEventArgs e)
{
    Log("Paused.");
    e.Tasks.Add(() => throwIfMaxDurationAsync(initialTime, TimeSpan.FromSeconds(2.0d)));
};

TimeSpan pauseInterval = TimeSpan.FromSeconds(0.5d);
await scheduler.RunAsync(pauseInterval);

The code is available for your perusal on GitHub: EventHandlerSample. Read, use, extend, or modify at will.

2 thoughts on “EventHandler and asynchrony

  1. ranyao

    Here could we define a “Task onPaused” as the “event publisher” to replace the Paused event and assign continuation tasks as event subscriber (onPaused.ContinueWith( … ) )? The event registration “+=” as well as Paused() here seems more for compatibility reason is it? Thanks a lot!

    1. Brian Rogers Post author

      Not quite. You’d have to accept a Func<Task> to do what you are proposing because the pause event publisher will not actually be a materialized Task, but rather an object that will eventually produce a Task sometime later. This post was showing how you can work within the bounds of .NET event semantics, so I suppose you could call that compatibility with standard patterns.

Leave a Reply

Your email address will not be published. Required fields are marked *