20

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?

George Harnwell
  • 784
  • 2
  • 9
  • 19
  • Possible duplicate of [Await on a completed task same as task.Result?](https://stackoverflow.com/questions/24623120/await-on-a-completed-task-same-as-task-result) – NineBerry Nov 14 '17 at 11:17
  • await will not return control but will also not block the thread. That's all it does. – zaitsman Nov 14 '17 at 11:18
  • 2
    @zaitsman technically, `await` on an incomplete task *does* return control - everything else is a continuation; the subtle thing is that *usually* the caller is well-behaved and knows that there's something on the back burner, and acts accordingly - it doesn't *have to*, though – Marc Gravell Nov 14 '17 at 12:22
  • RE:`//Some work that doesn't rely on the user object`. Then why not do this _before_ attempting to create/hydrate the user object? – user2051770 Aug 12 '22 at 21:29

4 Answers4

47

Let's make sure to not bury the lede here:

So for example: [some correct code] becomes [some incorrect code]

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

var userTask = _userRepo.GetByUsername(User.Identity.Name);    
//Some work that doesn't rely on the user object    
user = await _userRepo.UpdateLastAccessed(await userTask, DateTime.Now);    
return user;

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.

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 independent logic at the same time?

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-early await 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 using Result 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.
  • asynchronous function Foo awaits task1.
  • task1 is not yet complete, so Foo returns, allowing this thread to run more work. Foo returns a task representing its workflow, and signs up completing that task as the completion of task1.
  • The thread is now free to do work in the future, including task1.
  • task1 completes, triggering the execution of the completion of the workflow of Foo, and eventually completing the task representing the workflow of Foo.

Now suppose Foo instead fetches Result of task1. What happens? Foo synchronously waits for task1 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.

Eric Lippert
  • 647,829
  • 179
  • 1,238
  • 2,067
  • Yes, thank you Eric. I've already gone back and changed my code to await the Task, rather than do task.Result. I just misunderstood what .Result did but I understand now – George Harnwell Nov 14 '17 at 17:04
  • 2
    @GeorgeHarnwell: Excellent. Now consider: what is the difference between: `Foo(DateTime.Now, await userTask);` and `var user = await userTask; Foo(DateTime.Now, user);` ? Do you see why you might prefer one to the other? – Eric Lippert Nov 14 '17 at 18:34
  • 1
    The only thing I can see there is that I won't be able to inspect the user object whilst debugging in the first example, but I assume there's more to it? – George Harnwell Nov 15 '17 at 12:32
  • 2
    @GeorgeHarnwell: What happens if awaiting a long-running asynchronous task takes, oh, let's say three minutes? – Eric Lippert Nov 15 '17 at 13:54
  • @GeorgeHarnwell: The issue Eric is pointing out is not actually specific to async. The issue would be the same if you replaced `await userTask;` with a synchronous call to `GetUser()`. I'll also note that the same gotcha applies if a developer performs a "safe" signature refactor, changing the from `Foo(DateTime dt, string user)` to `Foo(string user, DateTime dt)` . – Brian Nov 15 '17 at 14:43
  • @GeorgeHarnwell: Partly due to the issue Eric is alluding to, I'm leery of passing `DateTime.Now` directly to functions. My preference is for functions which need to know the time to either call `DateTime.Now` (avoid an extra parameter) or be passed a variable (which was assigned `DateTime.Now`). The latter approach is appropriate if you have multiple operations which happened "now" and want all of the operations to have an identical DateTime (hopefully such grouped operations are not `public`; it's mildly dirty to include "tell me what time it is" in a public API). – Brian Nov 15 '17 at 14:47
  • @Brian: Sure, the problem is that "now" tells you what time it *used* to be, and you are right that the problem is not unique to awaitables. My point is that an awaitable is an awaitable *precisely because it is known to be high latency* and therefore more likely than your average call to produce a multi-second delay between call and result. And I totally agree that passing around the current time is a bad smell. – Eric Lippert Nov 15 '17 at 14:51
  • @Brian Passing through DateTime.Now was just a bit of example code I thew in to illustrate an example database call i.e. updating the last time a user accessed a system. In reality I wouldn't pass through the date time, I'd specify the date time at the instant I need it - in this case I'd be specifying it when executing the SQL statement to update a user. – George Harnwell Nov 16 '17 at 11:46
11

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

async Task<DateTime> CalculateSunSet()
{
    // start fetching sunset data. however don't wait for the result yet
    // you've got better things to do:
    Task<SunsetData> taskFetchData = FetchSunsetData();

    // because you are not awaiting your thread will do the following:
    Location location = FetchLocation();

    // now you need the sunset data, start awaiting for the Task:
    SunsetData sunsetData = await taskFetchData;

    // some big calculations are needed, that take 33 seconds,
    // you want to keep your caller responsive, so start a Task
    // this Task will be run by a different thread:
    Task<DateTime> taskBigCalculations = Taks.Run( () => BigCalculations(sunsetData, location);

    // again no await: you are still free to do other things
    ...
    // before returning you need the result of the big calculations.
    // wait until big calculations are finished, keep caller responsive:
    DateTime result = await taskBigCalculations;
    return result;
}
Harald Coppoolse
  • 28,834
  • 7
  • 67
  • 116
4

In your case, you can use:

user = await _userRepo.UpdateLastAccessed(await userTask, DateTime.Now);

or perhaps more clearly:

var user = await _userRepo.GetByUsername(User.Identity.Name);
//Some work that doesn't rely on the user object
user = await _userRepo.UpdateLastAccessed(user, DateTime.Now);

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 an async state machine and you think there's a good chance that the task has completed synchronously (perhaps using a local function for the async case), or if you're using callbacks rather than async/await, and you're inside the callback.

As an example of avoiding a state machine:

ValueTask<int> FetchAndProcess(SomeArgs args) {
    async ValueTask<int> Awaited(ValueTask<int> task) => SomeOtherProcessing(await task);
    var task = GetAsyncData(args);
    if (!task.IsCompletedSuccessfully) return Awaited(task);
    return new ValueTask<int>(SomeOtherProcessing(task.Result));
}

The point here is that if GetAsyncData returns a synchronously completed result, we completely avoid all the async machinery.

Marc Gravell
  • 1,026,079
  • 266
  • 2,566
  • 2,900
  • 1
    I am not sure I understand your code after the line "or perhaps more clerarly". Isn't it identical to the first example reported by the OP? – Luca Cremonesi Nov 15 '17 at 17:24
0

Have you considered this version?

var userTask = _userRepo.GetByUsername(User.Identity.Name);

//Some work that doesn't rely on the user object

user = await _userRepo.UpdateLastAccessed(await userTask, DateTime.Now);

return user;

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.

var userTask = _userRepo.GetByUsername(User.Identity.Name);

//Some work that doesn't rely on the user object

user = await userTask;
user = await _userRepo.UpdateLastAccessed(user, DateTime.Now);

return user;
NineBerry
  • 26,306
  • 3
  • 62
  • 93
  • Putting `await` inside the call is a bit weird. It makes debugging harder too - you can't check the return user object before it's passed to `UpdateLastAccessed` – Panagiotis Kanavos Nov 14 '17 at 11:21
  • @PanagiotisKanavos - so would userTask.Result throw an exception if the task hasn't completed? I assumed it would wait for the task to finish and then return the result (which looking back, doesn't really make sense). – George Harnwell Nov 14 '17 at 13:07
  • Result would wait for the Task to complete while blocking the current thread. The difference is that await does not block the current thread. – NineBerry Nov 14 '17 at 13:11
  • 1
    @PanagiotisKanavos: How is that any different from `Foo(Bar());` or `x = Foo() + Bar();`? There are lots of situations where its tricky to see the result of a subexpression; if you want to make those easier to debug, put the subexpression in an assignment statement and make a variable. – Eric Lippert Nov 14 '17 at 16:19
  • @GeorgeHarnwell: If the task has completed normally, `Result` produces the result. If the task has completed exceptionally, `Result` throws the exception. If the task has not completed, then `Result` *synchronously* waits for it to complete normally or exceptionally and either produces the value or throws the exception. **It is entirely possible and indeed likely that it waits forever due to a deadlock**. – Eric Lippert Nov 14 '17 at 16:20
  • @EricLippert that's what I meant – Panagiotis Kanavos Nov 14 '17 at 16:32
  • @GeorgeHarnwell are you referring to the comment to your question that was deleted for some reason and mentioned `GetAwaiter().GetResult()`? As that comment said, I wouldn't use any blocking code unless there was no way to avoid it, as with the Main() function of a console application, or when implementing a third-party interface which I can't change to return `Task`. At least `async void Main` was added in C# 7.1. In the inescapable case I'd prefer `GetAwaiter...` simply because it doesn't wrap the exceptions. – Panagiotis Kanavos Nov 14 '17 at 16:39
  • @GeorgeHarnwell even then, you can pull all async code to an inner method and use only a single blocking call right before returning – Panagiotis Kanavos Nov 14 '17 at 16:40