This is the fourth part of the Async Wandering series. For your convenience you can find other parts in the table of contents in Part 1 – Why creating Form from WinForms in unit tests breaks async?

C# allows us to write async function returning void. This is not recommended, however, sometimes it is required — especially when we are working with events. The question is, how do I attach asynchronous event handler and await for it?

First, let’s revisit some internal implementation of events and tasks.

Events

Event exposes two functions for attaching and removing event handler. This is why it is possible to have an event in an interface. In theory we could just implement event by defining these two functions and not using any event keyword at all.

However, events are closely related to delegates. Delegate is more or less a managed pointer to a function. It stores target instance, target method, and allows us to call the method in runtime. When we create delegate in source code, we are actually creating something inheriting MulticastDelegate which in turn is implemented using Delegate. Internally, event is implemented as a list of attached methods. This means that if you add method A, method B, and method A once again, next you call the event, it will call method A twice, however, no specific order is required by specification. Actual implementation calls handlers in order of their registration and removes them from the end, so if you remove method A, you will end up with method A and B, not B and A.

Delegate is allowed to point to a non-void method. The question is: if I have three methods attached to an event, what value will it return when I call it? It is worth asking only in C#, VB.NET doesn’t allow events to return values. Current implementation in C# returns last returned value.

Tasks

How does application know that some Task is able to continue or is finished? It’s all about synchronization context. Each thread has an instance of current synchronization context which it uses, and different frameworks use different implementations. For instance, WinForms uses synchronization context based on a message loop — when a Task is ready to continue, it posts message to synchronization context, which in turn posts it to message loop. This explains why hard waiting on a UI thread blocks async tasks. Don’t take this description as a detailed internal implementation, there are lot of quirks and gotchas which are not important here.

What is important is that we are able to implement custom synchronization context and replace it on a thread. We already saw this trick when we were fixing unit tests.

Asynchronous event handlers

One trick to wait for an event with asynchronous event handlers is to obtain event’s invocation list, call it by hand, get all returned tasks, and wait for them manually.

Unfortunately, this trick only applies to C#, since VB doesn’t allow events to return values. In that case we can modify event to accept another parameter which we use to return tasks manually. In event handler we can run new task and return it.

However, this is not always possible. If we are able to change event parameters, then we are probably able to remove it at all and simply replace with custom implementation allowing for tasks and all that stuff. We usually end up with events provided by UI frameworks which we cannot modify.

The trick (or rather dirty hack) is to replace synchronization context with custom implementation. Context has a method called when operation starts and matching method when operation ends. We can utilize this to keep track of started handlers and wait for them as long as it is possible. See the following implementation:

using System;
using System.Threading;
using System.Threading.Tasks;

public class MyContext : SynchronizationContext
{
    private readonly ManualResetEvent _signaler;
    public readonly Task Waiter;
    private int _taskCount;
    private bool _isDone;

    public MyContext()
    {
        _signaler = new ManualResetEvent(false );
        Waiter = Task.Run(() => _signaler.WaitOne());
    }

    public void Checkpoint()
    {
        _isDone = true;
        SignalIfDone();
    }

    private void SignalIfDone()
    {
        if (_taskCount == 0 && _isDone)
        {
            _signaler.Set();
        }
    }

    public override void OperationStarted()
    {
        _taskCount++;
    }

    public override void OperationCompleted()
    {
        _taskCount--;
        SignalIfDone();
    }
}

public static class DelegateHelper
{
    public static Task AwaitAsynchronousHandlers( this Delegate @delegate)
    {
        var context = new MyContext();
        var thread = new Thread(() => {
            SynchronizationContext.SetSynchronizationContext(context);
            @delegate.DynamicInvoke();
            context.Checkpoint();
        });
        thread.Start();
        return context.Waiter;
    }
}

delegate void Worker ();

public class Program
{
    static event Worker Workers;

    public static void Main()
    {
        Workers += async () => await Task.Delay(100).ContinueWith(t => Console.WriteLine(1));
        Workers += async () => await Task.Delay(150).ContinueWith(t => Console.WriteLine(2));
        Workers += async () => await Task.Delay(1600).ContinueWith(t => Console.WriteLine(3));
        Workers.AwaitAsynchronousHandlers().Wait();
    }
}

Let’s start reading the code from the bottom. We have delegate pointing to parameterless void method. We declare an event and attach to it three different asynchronous handlers. If we would raise the event in an ordinary way, we would not be able to await for tasks. That is why we implement custom helper for running event.

DelegateHelper starts from creating new thread. Since we need to replace synchronization context and we don’t want to mess up with current one in our thread, we create new thread and then perform all the dirty stuff. First, newly created thread replaces context. Next, it calls delegate, which invokes all attached event handlers. Event handlers run as far as they can go and return as soon as they are blocked. When DynamicInvoke method finishes, we can be pretty sure, that all event handlers were executed (well, as long as there were no exceptions) and all of them either finished or blocked. This means, that our custom synchronization context is already aware of all scheduled operations. We inform it, that this is the checkpoint by calling context.Checkpoint();.

And now the context implementation. We use ManualResetEvent and operations counter. When any new operation starts, we increment the counter. When it finishes, we decrement the counter and check whether we are done. We will be done only when the checkpoint is reached and the operations counter is zero. Beware, that there is no locking mechanism, so we might have there a race condition (this code is not production ready).

Context also exposes a task for awaiting. In our delegate helper we get this task and return it to the caller, so the caller can wait for it (either by calling .Wait() or by awaiting). Waiter finishes when reset event is set, and this happens only after reaching checkpoint and finishing all tasks.

Summary

This very naive implementation allows us to wait for void event handlers. Of course, this is another dirty hack which should not be used in a production code, however, sometimes we simply don’t have a choice.

We already seen how to await for a task in synchronous WinForms code, today we know how to wait for void event handlers. However, general rule of thumb is to use async all the way up and this is the solution we should strive for.