0

I need to run three async I/O operations in parallel, particularly they are the database calls. So, I write the following code:

// I need to know whether these tasks started running here
var task1 = _repo.GetThingOneAsync();
var task2 = _repo.GetThingTwoAsync();
var task3 = _repo.GetThingThreeAsync();

// await the results
var task1Result = await task1;
var task2Result = await task2;
var task3Result = await task3;

The GetThingOneAsync(), GetThingTwoAsync(), GetThingThreeAsync() methods are pretty much the similar to each other except that they have different return types(Task<string>, Task<int>, Task<IEnumerable<int>>). The example of one of the database calls is the following:

public async Task<IEnumerable<int>> GetThingOneAsync(string code)
{
    return await db.DrType.Where(t => t.Code == code).Select(t => t.IdType).ToListAsync();
}

In debug mode I can see that var task1 = _repo.GetThingOneAsync(); started to run GetThingOneAsync() async method (the same with other two tasks).

My colleagues say that _repo.GetThingOneAsync() does not start the async operation. They say that the operation started when we reach await (that statement seems to be wrong for me).

So, they suggest to fix my code to the following:

var task1 = _repo.GetThingOneAsync();
var task2 = _repo.GetThingTwoAsync();
var task3 = _repo.GetThingThreeAsync();

await Task.WhenAll(task1, task2, task3);

// then get the result of each task through `Result` property.

In my opinion, it's the same as I wrote in the very beginning of the question except that Task.WhenAll waits for later tasks to finish if an earlier task faults (this is Servy's comment from this question)

I know that my question is kind of duplicate but I want to know whether I'm doing things right or wrong.

marc_s
  • 732,580
  • 175
  • 1,330
  • 1,459
Dmitry Stepanov
  • 2,776
  • 8
  • 29
  • 45
  • 6
    _"My colleagues say that _repo.GetThingOneAsync() `does not start the async operation`. They say that the operation started when we reach await"_ - if by that they mean _"await is creating the Task"_ then your colleagues are incorrect. The `Task` has already started before control leaves the method. It has started whether you have `await`'d it or not. https://stackoverflow.com/q/43089372/585968 –  Jul 29 '19 at 12:30
  • Just make sure that your data access layer supports multithreading. For example the Entity framework [does not](https://stackoverflow.com/questions/9099359/entity-framework-and-multi-threading). – Theodor Zoulias Jul 29 '19 at 14:01

2 Answers2

10

My colleagues say that _repo.GetThingOneAsync() does not start the async operation. They say that the operation started when we reach await (that statement seems to be wrong for me).

They're wrong. The operation starts when the method is called.

This is easy to prove by starting an operation that has some observable side effect (like writing to a database). Call the method and then block the application, e.g., with Console.ReadKey(). You will then see the operation complete (in the database) without an await.

The remainder of the question is all about stylistic preference. There's a slight semantic difference between these options, but usually it's not important.

var task1 = _repo.GetThingOneAsync();
var task2 = _repo.GetThingTwoAsync();
var task3 = _repo.GetThingThreeAsync();

// await the results
var task1Result = await task1;
var task2Result = await task2;
var task3Result = await task3;

The code above will await (asynchronously wait) for each task to complete, one at a time. If all three complete successfully, then this code is equivalent to the Task.WhenAll approach. The difference is that if task1 or task2 have an exception, then task3 is never awaited.

This is my personal favorite:

var task1 = _repo.GetThingOneAsync();
var task2 = _repo.GetThingTwoAsync();
var task3 = _repo.GetThingThreeAsync();

await Task.WhenAll(task1, task2, task3);

var task1Result = await task1;
var task2Result = await task2;
var task3Result = await task3;

I like the Task.WhenAll because it's explicit. Reading the code, it's clear that it's doing asynchronous concurrency because there's a Task.WhenAll right there.

I like using await instead of Result because it's more resilient to code changes. In particular, if someone who doesn't like Task.WhenAll comes along and removes it, you still end up awaiting those tasks instead of using Result, which can cause deadlocks and wrap exceptions in AggregateException. The only reason Result works after Task.WhenAll is because those tasks have already been completed and their exceptions have already been observed.

But this is largely opinion.

Stephen Cleary
  • 437,863
  • 77
  • 675
  • 810
0

I agree with @MickyD, the tasks have been created on the initial call. The two calls are similar in effect.

A few nuances though. When you call GetThingOneAsync method, it executes up until the point where it reaches an await statement; that is when it returns the Task. If the Async method never does an await then it exits and returns an already-completed Task. So if these were compute-intensive routines (doesn't look like it) then you would not be achieving any parallelism. You would need to use Task.Run to achieve simultaneous execution. Another point is that if you use await from the UI thread then all of the execution will be on the UI thread -- just scheduled at different times. This is somewhat OK if the Task is doing IO because it will block for the read/write. However it can start to add up so if you are going to do anything substantial then you should put it on the thread pool (I.e. with Task.Run).

As for the comment from your colleagues, as I said, the task1,2,3 do start running before the awaits. But when you hit the await, the method that you are currently executing will suspend and return a Task. So it is somewhat correct that it is the await that creates the Task -- just that the task you are thinking about in your question (task1,2,3) is the one created when GetThingXxxAsync hits an await, not the one created when your main routine awaits task1,2,3.

sjb-sjb
  • 1,112
  • 6
  • 14
  • 1
    _This is somewhat OK if the Task is doing IO because it will block for the read/write_ This statement is wrong assuming the code properly awaits any IO. The entire point of async/await is to avoid blocking. Maybe you meant "release the thread" instead of "block"? – juharr Jul 29 '19 at 13:50
  • Well I meant that the Task is blocked until the I/O completes; the thread isn't blocked. – sjb-sjb Jul 29 '19 at 23:59