Search code examples
c#.netasync-awaitconfigureawait

How to correctly block on async code?


I have tons of code written in following manner:

public string SomeSyncOperation(int someArg)
{
   // sync code 
   SomeAsyncOperation(someArg, someOtherArg).ConfigureAwait(false).GetAwaiter().GetResult()
   // sync code
};

Here we have some sync code that have to access to async api, so it blocks until results are ready. We can't method change signature and add async here. So, we are waiting synchronously anyway, so do we need ConfigureAwait(false) here? I'm pretty sure that we don't, but I'm a bit affraid of removing it because it's probably covers some use cases (or why am I seeing it virtually everywhere? It's just a cargo cult?) and removing this call may lead to some unsafe results.

So does it makes sense at all?


Solution

  • How to correctly block on async code?

    You do not correctly block on async code. Blocking is wrong. Asking what the right way is to do the wrong thing is a non-starter.

    Blocking on async code is wrong because of the following scenario:

    • I have an object in hand representing an async operation.
    • The async operation is itself asynchronously waiting on the completion of a second async operation.
    • The second async operation will be scheduled to this thread when the message loop executes code associated with a message that is at present in this thread's message queue.

    And now you can figure out what goes horribly wrong when you attempt to fetch the result synchronously of the first async operation. It blocks until its child async operation is finished, which will never happen, because now we've blocked the thread that is going to service the request in the future!

    Your choices are:

    1. Make your entire call stack correctly asynchronous and await the result.
    2. Don't use this API. Write an equivalent synchronous API that you know does not deadlock, from scratch, and call it correctly.
    3. Write an incorrect program which sometimes deadlocks unpredictably.

    There are two ways to write a correct program; writing a synchronous wrapper over an asynchronous function is dangerous and wrong.

    Now, you might ask, didn't the ConfigureAwait solve the problem by removing the requirement that we resume on the current context? That's not the resumption point that we're worried about. If you're going to rely on ConfigureAwait to save you from deadlock then every asynchronous operation in the stack has to use it, and we don't know if the underlying asynchronous operation that is about to cause the deadlock did that!

    If the above is not entirely clear to you, read Stephen's article on why this is a bad practice, and why common workarounds are just dangerous hacks.

    Don't Block on Async Code

    and his updated article giving more hacks and workarounds here:

    Async Programming - Brownfield Async Development

    But again: the right thing to do is to redesign your program to embrace asynchrony and use await throughout. Don't try to work around it.

    becuase this method has stacktrace of ~20 methods, some of them are implementing some interfaces. Changing it to be async require change declarations in ~50 files, and we convert fully sync interfaces to mixed ones.

    Get busy then! This sounds pretty easy.