Search code examples
c#castingasync-awaitcontext-switchcovariant

What’s the overhead of await without I/O?


One downside of the async pattern in C# 5 is that Tasks are not covariant, i.e., there isn't any ITask<out TResult>.

I have noticed that my developers often do

return await SomeAsyncMethod();

to come around this.

Exactly what impact performance-wise will this create? There isn't any I/O or thread yield. It will just await and cast it to the correct covariant. What will the async framework do under the hood in this case? Will there be any Thread context switch?

This code won’t compile:

public class ListProductsQueryHandler : IQueryHandler<ListProductsQuery, IEnumerable<Product>>
{
    private readonly IBusinessContext _context;

    public ListProductsQueryHandler(IBusinessContext context)
    {
        _context = context;
    }

    public Task<IEnumerable<Product>> Handle(ListProductsQuery query)
    {
        return _context.DbSet<Product>().ToListAsync();
    }
}

because Task is not covariant, but adding await and it will cast it to the correct IEnumerable<Product> instead of List<Product> that ToListAsync returns.

ConfigureAwait(false) everywhere in the domain code does not feel like a viable solution, but I will certainly use it for my low-level methods like

public async Task<object> Invoke(Query query)
{
    var dtoType = query.GetType();
    var resultType = GetResultType(dtoType.BaseType);
    var handler = _container.GetInstance(typeof(IQueryHandler<,>).MakeGenericType(dtoType, resultType)) as dynamic;
    return await handler.Handle(query as dynamic).ConfigureAwait(false);
}

Solution

  • The more notable cost of your approach to solving this problem is that if there is a value in SynchronizationContext.Current you need to post a value to it and wait for it to schedule that work. If the context is busy doing other work, you could be waiting for some time when you don't actually need to do anything in that context.

    That can be avoided by simply using ConfigureAwait(false), while still keeping the method async.

    Once you've removed the possibility of using the sync context, then the state machine generated by the async method shouldn't have an overhead that's notably higher than what you'd need to provide when adding the continuation explicitly.