48

We're currently refactoring sections of our project to be async up and down, yay!

Due to our different understanding, me and a colleague (let's call him Jim), have differing opinions about how our async/await code will execute, and which way to write it.

Here is the example method Jim wrote:

public async Task<HouseModel> GetHouseModel(Guid houseId)
{
    House house = await _houseService.GetHouse(houseId);

    Task<IEnumerable<Furniture>> furniture = _furnitureService.GetFurnitureForHouse(house);

    Task<IEnumerable<Appliances>> appliances = _applianceService.GetAppliancesForHouse(house);

    return _houseModelFactory.MakeHouseModel(await furniture, await appliances);
}

And the example of how I would write it:

public async Task<HouseModel> GetHouseModel(Guid houseId)
{
    House house = await _houseService.GetHouse(houseId);

    IEnumerable<Furniture> furniture = await _furnitureService.GetFurnitureForHouse(house);

    IEnumerable<Appliances> appliances = await _applianceService.GetAppliancesForHouse(house);

    return _houseModelFactory.MakeHouseModel(furniture, appliances);
}

My understanding is: because the methods in both the furniture and appliance services in the above require House, they will wait for House to be available before continuing. Then, both methods that need House will run, but the second method (GetAppliancesForHouse) will not wait for the first to finish before starting.

Jim's understanding is: that we should await both methods only when they are needed. So that they will both run parallel to each other. He thinks that doing it my way will result in the second method waiting for the first, i.e.: GetAppliancesForHouse waiting for GetFurnitureForHouse.

Are any of these understandings correct? Or have we just been making it up as we go along? When should we await?

Jack Pettinger
  • 2,715
  • 1
  • 23
  • 37
  • Could you show us the source code for `GetFurnitureForHouse`? – mjwills Aug 22 '18 at 12:09
  • If you don't await an async method it will return, and execution of the calling code will continute, as soon as it hits the first await in the async method. – mm8 Aug 22 '18 at 12:10
  • If you do not need the result of an operation until later then I like Jim's approach. You can run operations in parallel. – Crowcoder Aug 22 '18 at 12:10
  • You can choose ConfigureAwait – Md. Abdul Alim Aug 22 '18 at 12:12
  • 19
    It's funny how this question and answers shows how misunderstood async/await is. When it's celebrated for how natural it is to previous methods. – Erndob Aug 22 '18 at 12:24
  • First half of [this Eric Lippert's answer](https://stackoverflow.com/a/47290354/11683) answers your question too. – GSerg Aug 22 '18 at 12:57
  • 3
    Possible duplicate of [multiple awaits vs Task.WaitAll - equivalent?](https://stackoverflow.com/questions/32119507/multiple-awaits-vs-task-waitall-equivalent) – Dmitry Pavliv Aug 22 '18 at 12:58
  • Bear in mind what `await` really does. It says "this method cannot continue until *whatever* is on the right hand side is finished". It doesn't do anything to *start* that thing to its right, it just depends on you writing an expression that gives it a `Task` or other awaitable. – Damien_The_Unbeliever Aug 22 '18 at 13:11
  • @Erndob - I know they struggled to find the right words to use for this concept, and couldn't find anything better than `await`. Despite that having a plain English meaning that is reasonably closely aligned with what it does, we still get people things that it *starts* async code running. Any idea what they could have done better? – Damien_The_Unbeliever Aug 22 '18 at 13:13
  • @Erndob Would you rather have a system that is so easy to use people can end up using it without understanding how it works, or a system so complex and tedious to use that even people with a good understanding of how it works can't or won't use it, because of how complex it is? Making things easier to use virtually always means that people end up using them without understanding them, it always happens in programming. That doesn't mean you don't want make things easier though. – Servy Aug 22 '18 at 13:54
  • 4
    How is that question "primarily opinion-based"? – Ivanka Todorova Aug 22 '18 at 16:29
  • Protip: always suffix the name of your async methods with the Async suffix. – Fred Sep 04 '18 at 11:46

2 Answers2

49

My understanding is: because the methods in both the furniture and appliance services in the above require House, they will wait for House to be available before continuing.

Your understanding is wrong. The methods that require House, they are not waiting for you to get House because you need it. They don't resolve their dependencies and when to wait for code or not on their own. The code waits to get Houses because you have await before it. It's not aware of what's going to happen next.

Then, both methods that need House will run, but the second method (GetAppliancesForHouse) will not wait for the first to finish before starting.

Similarly, the GetAppliancesForHouse won't have its own understanding if it should wait or not based on the dependencies. GetAppliancesForHouse won't run, because your code says to await GetFurnitureForHouse before it first. Your code will always run sequentially.

Jim's understanding is: that we should await both methods only when they are needed. So that they will both run parallel to each other.

That's generally true. As others have pointed out, the code still might run not in parallel depending on other factors. Also, there might be legitimate reasons to not want to run code in parallel.

He thinks that doing it my way will result in the second method waiting for the first, ie: GetAppliancesForHouse waiting for GetFurnitureForHouse.

He's right.

To see what happens exactly, you can put breakpoints and see what happens after each line. In Jims case, after going from Furniture to Appliances, furniture variable won't have the value yet, it's still a task, but you are already in the next line.

With your case, going to Appliances line, you will see that Furniture already has the value, since it waited for it.

Erndob
  • 2,512
  • 20
  • 32
  • 4
    @CamiloTerevinto is right. In addition, I'd point out that **Jim's way may introduce threading issues**. E.g. if EF is used for data access and *_applianceService* and *_furnitureService* share the same DbContext instance, there'll be problems as [DbContext is not thread-safe](https://stackoverflow.com/questions/6126616/is-dbcontext-thread-safe). – Adam Simon Aug 22 '18 at 12:52
  • 5
    @AdamSimon one always has to consider potential threading issues, that's why it is important to understand how async/await works. – Crowcoder Aug 22 '18 at 13:02
  • @Crowcoder Exactly. But there's so much confusion here about async/await, I considered it important to point out that this aspect must be taken into account, as well. – Adam Simon Aug 22 '18 at 13:10
  • I tried updating the answer based on your comments. – Erndob Aug 22 '18 at 13:11
  • @Crowcoder `await` has nothing to do with threading. If any of the task returning methods called create or use additional threads, then sure, multiple threads are involved and any shared data structures, like `house`, need to be synchronized. If there are no additional threads created, or there are no shared resources between threads (which is the most likely option) there is no thread synchronization to worry about. – Servy Aug 22 '18 at 13:50
  • 1
    @Servy of course it has to do with threading. I'm not saying multi-threading, I'm saying you have to know if you need to be context aware or not. – Crowcoder Aug 22 '18 at 13:55
  • @Crowcoder No, it doesn't. `await` has to do with asynchrony. Threading is only involved if you call methods that explicitly create or interact with other threads, which `await` doesn't do. If there is any code that has anything to do with threading, it's not in any of the code shown. – Servy Aug 22 '18 at 13:57
  • @Servy I mean that in the sense that when execution is awaiting, the thread that was doing work is released to potentially do other work. I'm **not** saying it is multi-threading. – Crowcoder Aug 22 '18 at 14:00
  • @Crowcoder That's not really threading issues, that's merely the behavior of the operations being run in a different order than they might appear, were this a synchronous solution. If the caller is then going on to mutate some form of shared state in a way that this method isn't expecting, then that would be an asynchrony issue, not a threading issue. – Servy Aug 22 '18 at 14:03
  • `To see what happens exactly, you can put breakpoints and see what happens after each line.` - Maybe C# has some way around this, but my experience is that since you're dealing with async code, a _breakpoint_ can disguise concurrency and make it look sequential. Print-based debugging might be more useful here. – Izkata Aug 22 '18 at 19:10
8

Neither of you is correct, see the answer by @erndob for the reasons. However, one of the questions is not answered:

When should we await?

  • Do you want the work to be done sequentially? Use your way.
  • Do you want the work to be done in parallel? Use Jim's way.

Note: Jim's way will not actually run in parallel if the Task Scheduler used is unable to run both Tasks at the same time, for example, due to lack of system resources (thanks @AdamSimon).

Camilo Terevinto
  • 31,141
  • 6
  • 88
  • 120
  • 1
    Respectfully, the OP states that their understanding is that "the second method (`GetAppliancesForHouse`) will not wait for the first to finish before starting." This is simply incorrect, and to say that it is not is misleading. – wchargin Aug 26 '18 at 06:56
  • @wchargin I had originally commented on that but the comment thread was cleaned up. I didn't comment on that on my answer because the OP does not (for me) say whether they are referring to their version (which would be incorrect) or Jim's (which would be correct) – Camilo Terevinto Aug 26 '18 at 13:35