Reputation: 7899
(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
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