Awaiting an async void method in .NET

 
 
  • Gérald Barré

async void methods are not a good way to define async methods. You should return a Task or ValueTask instead. The main point is to be able to await the method. But, is it possible to await an async void method? I don't say you should use async void in your code, I'm just questioning the point of not being able to await them…

When you create an async void method, the compiler generates code for you. You can see the generated code using a decompiler or Sharplab: async void example. The most interesting part is the usage of AsyncVoidMethodBuilder:

C#
public void MyAsyncMethod()
{
    <MyAsyncMethod>d__0 stateMachine = default(<MyAsyncMethod>d__0);
    stateMachine.<>t__builder = AsyncVoidMethodBuilder.Create();
    stateMachine.<>1__state = -1;
    stateMachine.<>t__builder.Start(ref stateMachine);
}

This AsyncVoidMethodBuilder.Create method starts by capturing the current SynchronizationContext (source code):

C#
public static AsyncVoidMethodBuilder Create()
{
    SynchronizationContext? sc = SynchronizationContext.Current;
    sc?.OperationStarted();
    return new AsyncVoidMethodBuilder() { _synchronizationContext = sc };
}

Then, the builder will execute the async state machine and report completion to the captured SynchronizationContext using SynchronizationContext.OperationCompleted() (source code):

C#
public void SetResult()
{
   // [code omitted for brevity]

   _builder.SetResult();
   if (_synchronizationContext != null)
   {
        NotifySynchronizationContextOfCompletion();
   }
}

private void NotifySynchronizationContextOfCompletion()
{
   try
   {
        _synchronizationContext.OperationCompleted();
   }
   catch (Exception exc)
   {
       // If the interaction with the SynchronizationContext goes awry,
       // fall back to propagating on the ThreadPool.
       Task.ThrowAsync(exc, targetContext: null);
   }
}

This means that we can implement a custom SynchronizationContext and use it before calling the async void method. Then, we can wait for the method to call OperationCompleted to know when the async operation completes.

C#
sealed class AsyncVoidSynchronizationContext : SynchronizationContext
{
    private static readonly SynchronizationContext s_default = new SynchronizationContext();

    private readonly SynchronizationContext _innerSynchronizationContext;
    private readonly TaskCompletionSource _tcs = new();
    private int _startedOperationCount;

    public AsyncVoidSynchronizationContext(SynchronizationContext? innerContext)
    {
        _innerSynchronizationContext = innerContext ?? s_default;
    }

    public Task Completed => _tcs.Task;

    public override void OperationStarted()
    {
        Interlocked.Increment(ref _startedOperationCount);
    }

    public override void OperationCompleted()
    {
        if (Interlocked.Decrement(ref _startedOperationCount) == 0)
        {
            _tcs.TrySetResult();
        }
    }

    public override void Post(SendOrPostCallback d, object? state)
    {
        Interlocked.Increment(ref _startedOperationCount);

        try
        {
            _innerSynchronizationContext.Post(s =>
            {
                try
                {
                    d(s);
                }
                catch (Exception ex)
                {
                    _tcs.TrySetException(ex);
                }
                finally
                {
                    OperationCompleted();
                }
            }, state);
        }
        catch (Exception ex)
        {
            _tcs.TrySetException(ex);
        }
    }

    public override void Send(SendOrPostCallback d, object? state)
    {
        try
        {
            _innerSynchronizationContext.Send(d, state);
        }
        catch (Exception ex)
        {
            _tcs.TrySetException(ex);
        }
    }
}

We can now use the custom synchronization context before calling the async void method and wait for the completion using the Completed property.

C#
public static async Task Run(Action action)
{
    var currentContext = SynchronizationContext.Current;
    var synchronizationContext = new AsyncVoidSynchronizationContext(currentContext);
    SynchronizationContext.SetSynchronizationContext(synchronizationContext);
    try
    {
        action();

        // Wait for the async void method to call OperationCompleted or to report an exception
        await synchronizationContext.Completed;
    }
    finally
    {
        // Reset the original SynchronizationContext
        SynchronizationContext.SetSynchronizationContext(currentContext);
    }
}

Here's how you can use the previous method to await an async void method:

C#
Console.WriteLine("before");
await Run(() => Test());
Console.WriteLine("after");

async void Test()
{
    Console.WriteLine("begin");
    await Task.Delay(1000);
    Console.WriteLine("end");
}

You can see that messages are in the expected order in the console:

#Additional Resources

Do you have a question or a suggestion about this post? Contact me!

Follow me:
Enjoy this blog?Buy Me A Coffee💖 Sponsor on GitHub