2013-02-28

Async OOP 5: Events

This post is going to deal with asynchronous event handlers. More specifically, how to design objects that will allow asynchronous event handlers.

For the vast majority of events, asynchronous event handlers work normally and we don't have to do anything special. Consider the classic Button.Click as a reasonable example: the producer of the event (Button) just raises the event normally, which starts calling the event subscribers one at a time. If there is an asynchronous handler, it will return early and the producer continues with the next event subscriber. Eventually, the asynchronous handler continues, usually returning to its original context, so it won't overlap the execution of any other event subscriber. In effect, asynchronous event handlers (usually) just get divided into a few actual events that each move the handler along a bit. This works perfectly well for the vast majority of events.

However, there's another class of events where this won't work. If the producer takes some special action when the event is completed, then the event is not really an event - it's a command. For example, the Application.Suspending event in Windows Store applications is an event that is raised when the application is being suspended; when the event handler returns, Windows assumes that the application is done preparing to be suspended. An asynchronous handler for this kind of "command-style event" isn't going to work well: the handler will return early and Windows will suspend the application, preventing the rest of the handler from executing.

Other examples exist, too: ASP.NET has page lifecycle "events", and when those handlers return, the page moves to the next step in the lifecycle. Class designs that implement the visitor pattern can also suffer from early returns - and this is true whether the visitor type is implemented using actual events or virtual methods that act like events. Some types just use (misuse?) events as implementations, e.g., BackgroundWorker.DoWork. In each of these situations, asynchronous event handlers are not going to behave as expected.

Normal events are notifications - the producer doesn't care whether there's a handler and there's no special semantic meaning to the completion of the event handlers. Normal events work fine if they're broadcasted, with no result returning to the producer. Other events are commands - the producer takes some action when the event handlers have completed. Command events with asynchronous handlers need to report some kind of completion or result back to the producer so that it knows the event handler has completed.

The WinRT Solution: Deferrals

With that background in mind, let's look at how WinRT solves this problem. For command-style events, the event producer must wait for a signal before considering the event handled. In the case of multiple asynchronous handlers, the producer must wait for all handlers to signal before considering the event handled. Also, it would be nice to solve this problem in a way that adds no overhead for synchronous event handlers.

WinRT introduces the concept of a "deferral" for command-style events. An asynchronous handler acquires a deferral before its first await, and notifies that deferral when the handler is complete. Synchronous handlers do not need to acquire deferrals. WinRT does not provide a "deferral manager" type for implementing our own command-style events, so we'll have to make our own.

There's already a type in my AsyncEx library with much of the functionality we need: AsyncCountdownEvent. We increment the count whenever a handler acquires a deferral and signal the count whenever a deferral is completed. To prevent race conditions, we also need to increment the count before invoking the handlers and signal the count when all the handlers have returned.

There is one difference in the design of WinRT deferrals and my deferral manager: WinRT provides different interfaces and implementations for each different deferral scenario (i.e., SuspendingDeferral has no relation to BackgroundTaskDeferral). Since a "deferral" only has one possible action (Complete), and since this action should always be done regardless of exceptions, I chose to represent deferrals as IDisposable. This allows asynchronous event handlers to use a using block in their implementation.

Compared to the design, the code is actually quite simple:

public sealed class DeferralManager
{
  private readonly AsyncCountdownEvent _count = new AsyncCountdownEvent(1);

  public IDisposable GetDeferral()
  {
    return new Deferral(_count);
  }

  public Task SignalAndWaitAsync()
  {
    _count.Signal();
    return _count.WaitAsync();
  }

  private sealed class Deferral : IDisposable
  {
    private AsyncCountdownEvent _count;

    public Deferral(AsyncCountdownEvent count)
    {
      _count = count;
      _count.AddCount();
    }

    void IDisposable.Dispose()
    {
      if (_count == null)
        return;
      _count.Signal();
      _count = null;
    }
  }
}
The DeferralManager in the AsyncEx library is almost identical to this code, except that it lazy-creates the asynchronous countdown event. This minimizes overhead if all the handlers are synchronous.

Once you have a DeferralManager, you can extend your "command-style" event arguments type as such:

public class MyEventArgs : EventArgs
{
  private readonly DeferralManager deferrals = new DeferralManager();

  ... // Your own constructors and properties.

  public IDisposable GetDeferral()
  {
    return deferrals.GetDeferral();
  }

  internal Task WaitForDeferralsAsync()
  {
    return deferrals.SignalAndWaitAsync();
  }
}

Then, each time you raise the event, use code similar to this:

private Task RaiseMyEventAsync()
{
  var handler = MyEvent;
  if (handler == null)
    return Task.FromResult<object>(null); // or TaskConstants.Completed

  var args = new MyEventArgs(...);
  handler(args);
  return args.WaitForDeferralsAsync();
}

One final note: ensure that your event arguments type (MyEventArgs) supports concurrent access. If there are multiple asynchronous handlers, they all will share a reference to the same event arguments instance. Ideally, all the properties on that type should be immutable.

The Task-Returning Delegate Solution

I think deferrals are a perfectly acceptable solution. It's the solution that was chosen for WinRT, so it's one that programmers will be familiar with.

However, there's another solution that should work as well: make your event handler delegate type return Task instead of void. This is not as evil as it first appears; it's true that event handlers are supposed to return void, but Task is the async equivalent of void, so the event handler delegate type still logically returns void, even though it doesn't literally return void.

If you take this approach, you could use Delegate.GetInvocationList to invoke each handler individually and Task.WhenAll to detect when they have all completed.

The advantage of this approach is that there's no need for deferrals (or any explicit code) in the event handlers. The disadvantage is that all event handlers are forced to have an asynchronous signature; this means synchronous event handlers would have to return Task.FromResult<object>(null) or some such. This is both awkward and inefficient.

The SynchronizationContext Solution

OK, I suppose there is a third solution, if you really want to go there.

Consider the example of ASP.NET page lifecycle events. With this example, it would have been a lot of work to add deferrals to the existing events, and that solution would probably bring up backwards-compatibility issues. So ASP.NET took a different approach: they integrated their events with the ASP.NET SynchronizationContext.

My Feb 2011 MSDN article describes how async methods interact with SynchronizationContext; at that time async was in a CTP stage, but that interaction did not change when async was released in VS2012. To summarize: async void methods call SynchronizationContext.OperationStarted at the beginning of the method and SynchronizationContext.OperationCompleted at the end of the method. The ASP.NET SynchronizationContext uses these methods to keep track of how many operations are currently in progress.

So, the ASP.NET page lifecycle events use this information: after raising each event, the ASP.NET runtime (asynchronously) waits until the operation count returns to its previous level. At that point, it knows that any async event handlers have run to completion.

But don't do that. Yes, it's cool, and very clever. But having your component override the SynchronizationContext for your event handlers is almost certainly a bad idea. It makes sense for ASP.NET because ASP.NET is a platform, not a library.

You Should Use Deferrals

I recommend using deferrals. It's what programmers are expecting these days. I just mentioned these other solutions for the sake of completeness.

4 comments:

  1. This is further evidence that async, as cool as it is, will never be on par with sync code when it comes to productivity.

    In scenarios like ASP.NET, when there are no long-running handlers in play, the only sane advice is to not use async if there is a choice.

    ReplyDelete
    Replies
    1. Synchronous code is the easiest to understand, but it's easier to create *better* (responsive / scalable) programs with async than sync. Event handlers were designed from a sync perspective, and that's why using them with async is awkward.

      More modern approaches like Rx really reveal the drawbacks of C# events. I think if events were added to C# now, they would look very, very different (and naturally work great with async).

      Delete
    2. I'd venture to say that almost any real ASP.NET application does not have the characteristics that make async necessary (and that cannot be fixed with increasing thread and connection limits to insane (but working) levels). The obvious exceptions are SignalR-style apps and apps with very-high-latency backend services which still support a lot of throughput. Very rare and in those cases I'd just use async for a small part of the code.

      Your comments regarding events and Rx are true. Still, async has fundamental debugging problems. For example the logical stack is not available and you can't pause the debugger to see what all the (IO-based) tasks are doing. It is actually quite awful if you're used to inspecting a running system.

      Delete
    3. Well, it depends. :) I think asynchronous code is beneficial (maybe not necessary, but beneficial) to most ASP.NET code because most ASP.NET apps are I/O-bound. Sure, someone can tweak the thread pool if they need to scale, but how many ASP.NET programmers know to do that? And even with tweaking, async code can scale an order of magnitude higher than threads.

      However, I definitely have to agree regarding debugging. FYI, there are ETW events that were added to track async/await. They're not documented yet, but they're in the runtime. I expect that we'll see some better visualizations and debugging helpers in future updates that build on this.

      Delete