Xavier Peña
Xavier Peña

Reputation: 7899

Consequences of making an Async function synchronous

(I apologize in advance if some part/parts of the question are considered "too basic". I've done my research about async-await, but for me it is not an easy concept to grasp. Either that or I haven't found the right resources.)

I've always read (or that is what I interpreted) that "the way to make an asynchornous call synchronous is to put 'await' in front of it".

So the code resulting from this statement is as shown in this simple code:

// CODE 1:
public void async MyFunction()
{
    await SomeFunctionAsync();
}

That is: 1) put await in front of the Async function, 2) put async in your function, because now it contains an await.

But in my experience sometimes behaves a bit "funnily". The symptoms I've noticed are that sometimes it behaves like a thread that escapes my control.

So the first question would be: what happens when you call MyFunction() and this function contains the async keyword? My understanding is that (confusingly enough) it should not be called MyFunctionAsync() because that is reserved for functions that return a Task.

...so the alternative that I've come up with (that seems to behave like a synchronous class consistently) is this type of code:

// CODE 2:
public void MyFunction()
{
    var task = SomeFunctionAsync();
    Task.WaitAll(task);
}

Second question: could someone explain the downsides (if any) of CODE 2?

Also, the implicit question is that I am interested in knowing about any misconception that I might have about the async-await pattern.

Upvotes: 1

Views: 610

Answers (1)

Stephen Cleary
Stephen Cleary

Reputation: 456322

I've always read (or that is what I interpreted) that "the way to make an asynchornous call synchronous is to put 'await' in front of it".

Not at all. This is a serial way to call asynchronous code, but not a synchronous say. "Synchronous" means "blocking the current thread", and await won't block the current thread. However, "serial" means "do this before the next thing", and await will do that. For more information, see my async intro.

Using await is the most natural and common way to call an asynchronous method. Note that normally when you add an async to the method, you should also change the return type from void to Task. The compiler will guide you in this: if you just put the await in without the async, it will suggest the next change for you very explicitly.

But in my experience sometimes behaves a bit "funnily". The symptoms I've noticed are that sometimes it behaves like a thread that escapes my control.

This is because your code is using async void and not async Task. async void is unnatural; you have no "control" because you're not returning a Task. The more natural approach is to make the method async Task, and await that task from it's callers, and so on. It is natural for async to "grow" through your stack like this - what we call "async all the way".

So the first question would be: what happens when you call MyFunction() and this function contains the async keyword? My understanding is that (confusingly enough) it should not be called MyFunctionAsync() because that is reserved for functions that return a Task.

Your understanding regarding naming is correct. MyFunction is not a natural (Task-returning) asynchronous method.

async void methods are very strange and do not behave like normal async methods. Their odd behavior is one reason why I recommend to avoid async void. But since you asked...

An await inside an async void method works just like an await in a regular async method (as I describe in my async intro): it will capture the current context (the current SynchronizationContext or TaskScheduler, and resume executing the async void method in that context when the asynchronous operation completes. This is why it acts like a "thread outside your control" - it just resumes executing whenever it needs to. You have no way of detecting when it's complete because the method returned void instead of Task.

Exceptions are where things get even more weird. async void methods capture the current SynchronizationContext at the beginning of their execution, and if an exception escapes the async void method, it'll re-raise that exception on its captured SynchronizationContext. This generally causes a process-level crash.

Second question: could someone explain the downsides (if any) of CODE 2?

It can easily cause a deadlock situation that I describe on my blog. It will also wrap any exceptions from the task in an AggregateException.

The bottom line is: don't block on asynchronous code. If you're going to block on it anyway, then why make it asynchronous in the first place? Any mixed blocking-and-async code should be considered technical debt of a rather high priority. There's no solution that works in all cases, but there are a variety of hacks that you can temporarily use while upgrading your code to be properly asynchronous; I describe them in an article on brownfield async development.

Upvotes: 4

Related Questions