Whilst I've been using async code in .NET for a while, I've only recently started to research it and understand what's going on. I've just been going through my code and trying to alter it so if a task can be done in parallel to some work, then it is. So for example:
var user = await _userRepo.GetByUsername(User.Identity.Name);
//Some minor work that doesn't rely on the user object
user = await _userRepo.UpdateLastAccessed(user, DateTime.Now);
return user;
Now becomes:
var userTask = _userRepo.GetByUsername(User.Identity.Name);
//Some work that doesn't rely on the user object
user = await _userRepo.UpdateLastAccessed(userTask.Result, DateTime.Now);
return user;
My understand is that the user object is now being fetched from the database WHILST some unrelated work is going on. However, things I've seen posted imply that result should be used rarely and await is preferred but I don't understand why I'd want to wait for my user object to be fetched if I can be performing some other independant logic at the same time?
Let's make sure to not bury the lede here:
NEVER NEVER NEVER DO THIS.
Your instinct that you can restructure your control flow to improve performance is excellent and correct. Using
Result
to do so is WRONG WRONG WRONG.The correct way to rewrite your code is
Remember, await does not make a call asynchronous. Await simply means "if the result of this task is not yet available, go do something else and come back here after it is available". The call is already asynchronous: it returns a task.
People seem to think that
await
has the semantics of a co-call; it does not. Rather, await is the extract operation on the task comonad; it is an operator on tasks, not call expressions. You normally see it on method calls simply because it is a common pattern to abstract away an async operation as a method. The returned task is the thing that is awaited, not the call.Why do you believe that using
Result
will allow you to perform other independent logic at the same time??? Result prevents you from doing exactly that. Result is a synchronous wait. Your thread cannot be doing any other work while it is synchronously waiting for the task to complete. Use an asynchronous wait to improve efficiency. Remember,await
simply means "this workflow cannot progress further until this task is completed, so if it is not complete, find more work to do and come back later". A too-earlyawait
can, as you note, make for an inefficient workflow because sometimes the workflow can progress even if the task is not complete.By all means, move around where the awaits happen to improve efficiency of your workflow, but never never never change them into
Result
. You have some deep misunderstanding of how asynchronous workflows work if you believe that usingResult
will ever improve efficiency of parallelism in the workflow. Examine your beliefs and see if you can figure out which one is giving you this incorrect intuition.The reason why you must never use
Result
like this is not just because it is inefficient to synchronously wait when you have an asynchronous workflow underway. It will eventually hang your process. Consider the following workflow:task1
represents a job that will be scheduled to execute on this thread in the future and produce a result.task1
.task1
.task1
completes, triggering the execution of the completion of the workflow ofFoo
, and eventually completing the task representing the workflow ofFoo
.Now suppose
Foo
instead fetchesResult
oftask1
. What happens?Foo
synchronously waits fortask1
to complete, which is waiting for the current thread to become available, which never happens because we're in a synchronous wait. Calling Result causes a thread to deadlock with itself if the task is somehow affinitized to the current thread. You can now make deadlocks involving no locks and only one thread! Don't do this.Have you considered this version?
This will execute the "work" while the user is retrieved, but it also has all the advantages of
await
that are described in Await on a completed task same as task.Result?As suggested you can also use a more explicit version to be able to inspect the result of the call in the debugger.
Async await does not mean that several threads will be running your code.
However, it will lower the time your thread will be waiting idly for processes to finish, thus finishing earlier.
Whenever the thread normally would have to wait idly for something to finish, like waiting for a web page to download, a database query to finish, a disk write to finish, the async-await thread will not be waiting idly until the data is written / fetched, but looks around if it can do other things instead, and come back later after the awaitable task is finished.
This has been described with a cook analogy in this inverview with Eric Lippert. Search somewhere in the middle for async await.
Eric Lippert compares async-await with one(!) cook who has to make breakfast. After he starts toasting the bread he could wait idly until the bread is toasted before putting on the kettle for tea, wait until the water boils before putting the tea leaves in the teapot, etc.
An async-await cook, wouldn't wait for the toasted bread, but put on the kettle, and while the water is heating up he would put the tea leaves in the teapot.
Whenever the cook has to wait idly for something, he looks around to see if he can do something else instead.
A thread in an async function will do something similar. Because the function is async, you know there is somewhere an await in the function. In fact, if you forget to program the await, your compiler will warn you.
When your thread meets the await, it goes up its call stack to see if it can do something else, until it sees an await, goes up the call stack again, etc. Once everyone is waiting, he goes down the call stack and starts waiting idly until the first awaitable process is finished.
After the awaitable process is finished the thread will continue processing the statements after the await until he sees an await again.
It might be that another thread will continue processing the statements that come after the await (you can see this in the debugger by checking the thread ID). However this other thread has the context of the original thread, so it can act as if it was the original thread. No need for mutexes, semaphores, IsInvokeRequired (in winforms) etc. For you it seems as if there is one thread.
Sometimes your cook has to do something that takes up some time without idly waiting, like slicing tomatoes. In that case it might be wise to hire a different cook and order him to do the slicing. In the mean time your cook can continue with the eggs that just finished boiling and needed peeling.
In computer terms this would be if you had some big calculations without waiting for other processes. Note the difference with for instance writing data to disk. Once your thread has ordered that the data needs to be written to disk, it normally would wait idly until the data has been written. This is not the case when doing big calculations.
You can hire the extra cook using
Task.Run
In your case, you can use:
or perhaps more clearly:
The only time you should touch
.Result
is when you know the task has been completed. This can be useful in some scenarios where you are trying to avoid creating anasync
state machine and you think there's a good chance that the task has completed synchronously (perhaps using a local function for theasync
case), or if you're using callbacks rather thanasync
/await
, and you're inside the callback.As an example of avoiding a state machine:
The point here is that if
GetAsyncData
returns a synchronously completed result, we completely avoid all theasync
machinery.