How to await an event handler

One of the known problems with the async/await pattern, is that it doesn't interop well with events. In order to properly await an async method, the method should return a Task (or some other awaitable). However, the common pattern for event handlers is that they return void. In fact, .NET 4 introduced the generic EventHandler<TEventArgs> delegate, to save us the repeated task of writing a delegate to a method accepting an object and an EventArgs.

While a void method can be async, it can't be awaited, and return before it is completed. As a result, the method that called the event handler will continue execution before the handler is complete, and may not receive the result (usually set by the handler in the EventArgs argument). Fortunately, there is a way out of it.

First, let's review the common pattern of an event publisher and subscriber.

Publisher

    class Publisher
    {
        public event EventHandler<EventArgs> SomethingHappened;

        protected virtual void OnSomethingHappened(EventArgs args)
        {
            Console.WriteLine("Something Happened");
            SomethingHappened?.Invoke(this, args);
            Console.WriteLine("Event Complete");
        }

        public void DoSomething()
        {
            OnSomethingHappened(EventArgs.Empty);
        }
    }

Subscriber

   class Subscriber
    {
        private Publisher _publisher = new Publisher();
        public Subscriber()
        {
            _publisher.SomethingHappened += _publisher_SomethingHappened;
        }

        private void _publisher_SomethingHappened(object sender, EventArgs e)
        {
            //do something when something happened.
            Console.WriteLine("Event is handled");
        }

        public void Start()
        {
            _publisher.DoSomething();
            Console.ReadKey();
        }
    }

The output from this program is:

Something Happened
Event is handled
Event Complete

Notice the null-propagation operator, "?.", introduced in C# 6, making the null check much easier.

Now, suppose we want to call some async method in our event handler. We want to properly await our async method, so we'll change _publisher_SomethingHappened to be async.

    class Subscriber
    {
        private Publisher _publisher = new Publisher();
        public Subscriber()
        {
            _publisher.SomethingHappened += _publisher_SomethingHappened;
        }

        private async void _publisher_SomethingHappened(object sender, EventArgs e)
        {
            await DoSomethingAsync();
            Console.WriteLine("Event is handled");
        }

        private async Task DoSomethingAsync()
        {
            Console.WriteLine("Doing something async");
            await Task.Delay(500);
        }
        public void Start()
        {
            _publisher.DoSomething();
            Console.ReadKey();
        }
    }

The output now is:

Something Happened
Doing something async
Event Complete
Event is handled

As we can see, the code in the OnSomethingHappened method continued execution before our handler completed, because we can't really await a method which returns void.

The solution is, to have an event which accepts handlers that return a Task, which will alow us to properly await them. Since the built-in EventHandler<TEventArgs> represents methods which return void, we will have to define a new delegate, let's call that AsyncEventHandler, which represents methods returning a Task:

public delegate Task AsyncEventHandler<in TEventArgs>(Object sender, TEventArgs args) where TEventArgs : EventArgs;

Next, we will change our event handler to return a Task, and similarly, the call chain to OnSomethingHappened (now properly renamed to OnSomethingHappenedAsync). Notice that we can no longer use the null-propagation operator, because if the event is null, await will throw a NullReferenceException.

Main

    class Program
    {
        static async Task Main(string[] args)
        {
            Subscriber subscriber = new Subscriber();
            await subscriber.StartAsync();
        }
    }

Publisher

    public delegate Task AsyncEventHandler<in TEventArgs>(Object sender, TEventArgs args) where TEventArgs : EventArgs;

    class Publisher
    {
        public event AsyncEventHandler<EventArgs> SomethingHappened;

        protected virtual async Task OnSomethingHappened(EventArgs args)
        {
            Console.WriteLine("Something Happened");
            AsyncEventHandler<EventArgs> handler = SomethingHappened;
            if (handler != null)
            {
                await handler(this, args);
            }
            Console.WriteLine("Event Complete");
        }

        public async Task DoSomethingAsync()
        {
            await OnSomethingHappened(EventArgs.Empty);
        }
    }

Notice the new async Main, introduced in C# 7.1.

Subscriber

    class Subscriber
    {
        private Publisher _publisher = new Publisher();
        public Subscriber()
        {
            _publisher.SomethingHappened += _publisher_SomethingHappened;
        }

        private async Task _publisher_SomethingHappened(object sender, EventArgs e)
        {
            await DoSomethingAsync();
            Console.WriteLine("Event is handled");
        }

        private async Task DoSomethingAsync()
        {
            Console.WriteLine("Doing something async");
            await Task.Delay(500);
        }
        public async Task StartAsync()
        {
            await _publisher.DoSomethingAsync();
            Console.ReadKey();
        }
    }

Now, our event is raised and awaited correctly, and executed at the expected order:

Something Happened
Doing something async
Event is handled
Event Complete

But we are not done yet. The problem with the above code, is that it works fine as long as you have only one subscriber. If more than one handler is subscribed to the event, the OnSomethingHappenedAsync will continue after the first handler is complete, not waiting for the other handlers. The reason is that when we invoke the event, it calls the handlers in order, returning us the result. After the first handler returns a completed task, await does what it does in this case, and continues execution, not knowing there are other handlers that haven't finished yet. We want to await all of them before we continue.

Before I introduce the solution to that, we need to understand a small detail about events and delegates. When you define a delegate, you actually derive from the Delegate class, which represents a single method. When you define an event, you actually derive from the MulticastDelegate class, which inherits Delegate, and can represent multiple methods. The methods of a MulticastDelegate are referenced from the internal invocation list, which is linked list of pointers. You can get a copy of this list through the GetInvocationList() method.

With that in mind, lets see how we can make sure all handlers are complete before we continue.

Publisher

    public delegate Task AsyncEventHandler<in TEventArgs>(Object sender, TEventArgs args) where TEventArgs : EventArgs;

    class Publisher
    {
        public event AsyncEventHandler<EventArgs> SomethingHappened;

        protected virtual async Task OnSomethingHappened(EventArgs args)
        {
            Console.WriteLine("Something Happened");
            AsyncEventHandler<EventArgs> handler = SomethingHappened;
            if (handler != null)
            {
                IEnumerable<AsyncEventHandler<EventArgs>> handlers = handler.GetInvocationList().Cast<AsyncEventHandler<EventArgs>>();
                List<Task> tasks = new List<Task>(handlers.Count());
                foreach (AsyncEventHandler<EventArgs> asyncHandler in handlers)
                {
                    tasks.Add(asyncHandler(this, args));
                }
                await Task.WhenAll(tasks);
            }
            Console.WriteLine("Event Complete");
        }

        public async Task DoSomethingAsync()
        {
            await OnSomethingHappened(EventArgs.Empty);
        }
    }

Subscriber

    class Subscriber
    {
        private Publisher _publisher = new Publisher();
        public Subscriber()
        {
            _publisher.SomethingHappened += _publisher_SomethingHappened_1;
            _publisher.SomethingHappened += _publisher_SomethingHappened_2;
            _publisher.SomethingHappened += _publisher_SomethingHappened_3;
        }

        private async Task _publisher_SomethingHappened_1(object sender, EventArgs e)
        {
            await DoSomethingAsync(1);
            Console.WriteLine("Event 1 is handled");
        }

        private async Task _publisher_SomethingHappened_2(object sender, EventArgs args)
        {
            await DoSomethingAsync(2);
            Console.WriteLine("Event 2 is handled");
        }

        private async Task _publisher_SomethingHappened_3(object sender, EventArgs args)
        {
            await DoSomethingAsync(3);
            Console.WriteLine("Event 3 is handled");
        }

        private async Task DoSomethingAsync(int handlerId)
        {
            Console.WriteLine("Doing something async " + handlerId);
            await Task.Delay(500);
        }

        public async Task StartAsync()
        {
            await _publisher.DoSomethingAsync();
            Console.ReadKey();
        }
    }

I've added two more handlers to the event, to demonstrate multiple subscribers.

What we've done, is iterate over the list of handlers, and instead of letting the event invoke them, we called them ourselves, collecting the returned tasks, and awaiting them all before we continue. The output is as expected, with the event completed after all handlers are completed:

Something Happened
Doing something async 1
Doing something async 2
Doing something async 3
Event 3 is handled
Event 2 is handled
Event 1 is handled
Event Complete

When we execute tasks concurrently, we have no guarantee about the order of execution. In face, they may be executed in parallel, which is why we see the output from the handlers in a different order than the order in which they are subscribed (and launched).

In order to save us from repeating this code on every async event, let's wrap this in an extension method:

    public delegate Task AsyncEventHandler<in TEventArgs>(Object sender, TEventArgs args) where TEventArgs : EventArgs;

    class Publisher
    {
        public event AsyncEventHandler<EventArgs> SomethingHappened;

        protected virtual async Task OnSomethingHappened(EventArgs args)
        {
            Console.WriteLine("Something Happened");
            AsyncEventHandler<EventArgs> handler = SomethingHappened;
            if (handler != null)
            {
                await handler.InvokeAsync(this, args);
            }
            Console.WriteLine("Event Complete");
        }

        public async Task DoSomethingAsync()
        {
            await OnSomethingHappened(EventArgs.Empty);
        }
    }

    static class EventHandlerExtensions
    {
        public static Task InvokeAsync<TEventArgs>(this AsyncEventHandler<TEventArgs> handler, Object sender, TEventArgs args)
            where TEventArgs : EventArgs
        {
            IEnumerable<AsyncEventHandler<EventArgs>> handlers = handler.GetInvocationList().Cast<AsyncEventHandler<EventArgs>>();
            List<Task> tasks = new List<Task>(handlers.Count());
            foreach (AsyncEventHandler<EventArgs> asyncHandler in handlers)
            {
                tasks.Add(asyncHandler(sender, args));
            }
            return Task.WhenAll(tasks);
        }
    }

Now, we just have to remember to call InvokeAsync to execute the handlers, instead of calling the event directly, or it's Invoke method.

Fortunately, you don't have to write all of this code again, because it's nicely encapsulated in a library named AsyncEvent you can grab from NuGet.

הפוסט הזה פורסם בקטגוריה general.‏ קישור ישיר לפוסט.

כתיבת תגובה

האימייל לא יוצג באתר. שדות החובה מסומנים *

WP-SpamFree by Pole Position Marketing