4

I've seen how the await keyword is implemented and resulting structure it creates. I think I have a rudimentary understanding of it. However, is

public async Task DoWork()
{
    await this.Operation1Async();
    await this.Operation2Async();
    await this.Operation3Async();
}

"better" (generally speaking) or

public async Task DoWork()
{
    await this.Operation1Async();
    this.Operation2();
    this.Operation3();
}

The problem with the first approach is that it is creating a new Task for each await call? Which entails a new thread?

Whereas the first creates a new Task on the first await and then everything from there is processed in the new Task?

Edit Ok maybe I wasn't too clear, but if for example we have

while (await reader.ReadAsync())
{
    //...
}

await reader.NextResultAsync();

// ...

Is this not creating two tasks? One in the main thread with the first ReadAsync then another task in this newly created task with the NextResultAsync. My question is there really a need for the second task, isn't the one task created in the main thread sufficient? So

while (await reader.ReadAsync())
{
    //...
}

reader.NextResult();

// ...
Umair
  • 3,063
  • 1
  • 29
  • 50

4 Answers4

5

it is creating a new Task for each await call? Which entails a new thread?

Yes and no. Yes, it is creating a Task for each asynchronous method; the async state machine will create one. However, these tasks are not threads, nor do they even run on threads. They do not "run" anywhere.

You may find some blog posts of mine useful:

  • async intro, which explains how async/await work.
  • There Is No Thread, which explains how tasks can work without threads.
  • Intro to the Task type, which explains how some tasks (Delegate Tasks) have code and run on threads, but the tasks used by async (Promise Tasks) do not.

Whereas the first creates a new Task on the first await and then everything from there is processed in the new Task?

Not at all. Tasks only complete once, and the method will not continue past the await until that task is complete. So, the task returned by Operation1Async has already completed before Operation2 is even called.

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

The 2 examples are not functionally equivalent so you would choose the one depending on your specific needs. In the first example the 3 tasks are executed sequentially, whereas in the second example the second and third tasks are running in parallel without waiting for their result to complete. Also in the second example the DoWork method could return before the second and third tasks have completed.

If you want to ensure that the tasks have completed before leaving the DoWork method body you might need to do this:

public async Task DoWork()
{
    await this.Operation1Async();
    this.Operation2().GetAwaiter().GetResult();
    this.Operation3().GetAwaiter().GetResult();
}

which of course is absolutely terrible and you should never be doing it as it is blocking the main thread in which case you go with the first example. If those tasks use I/O completion ports then you should definitely take advantage of them instead of blocking the main thread.

If on the other hand you are asking whether you should make Operation2 and Operation3 asynchronous, then the answer is this: If they are doing I/O bound stuff where you can take advantage of I/O Completion Ports then you should absolutely make them async and go with the first approach. If they are CPU bound operations where you cannot use IOCP then it might be better to leave them synchronous because it wouldn't make any sense to execute this CPU bound operations in a separate task which you would block for anyway.

Darin Dimitrov
  • 1,023,142
  • 271
  • 3,287
  • 2,928
  • 1
    It is probably meant that Operation2 and Operation3 are non-async methods in the second example. – Eugene Podskal Apr 17 '16 at 10:54
  • @EugenePodskal, according to his first example they are pretty async, checkout how he is awaiting on them. – Darin Dimitrov Apr 17 '16 at 10:55
  • 1
    That's probably the question we should ask the OP. But Operation2Async and Operation2 seem to be two distinct methods. Or it may be just a typo. – Eugene Podskal Apr 17 '16 at 10:57
  • Hmm, you are right. It might indeed be a different method. In this case the question doesn't make any sense because the 2 are completely different things. I will update my answer to cover this possibility as well. Thanks for pointing this out. – Darin Dimitrov Apr 17 '16 at 10:57
  • I see your amended question. I repeat once again what I said multiple times. If the operation is I/O bound then you should use the first approach with multiple awaits. Even if this uses multiple tasks they are delegated to the underlying I/O Completion Port in the OS and thus they are extremely short living. This is much more efficient compared to the second approach in which you are writing a blocking call. – Darin Dimitrov Apr 18 '16 at 07:16
1

The problem with the first approach is that it is creating a new Task for each await call? Which entails a new thread?

This is your misunderstanding, which is leading to you to be suspicious of the code in the first example.

A Task does not entail a new thread. A Task certainly can be run on a new thread if you want to do so, but an important use of tasks is when a task directly or indirectly works through asynchronous i/o, which happens when the task, or one that it in turn awaits on, uses async i/o to access files or network streams (e.g. web or database access) allowing a pooled thread to be returned to the pool until that i/o has completed.

As such if the task does not complete immediately (which may happen if e.g. its purpose could be fulfilled entirely from currently-filled buffers) the thread currently running it can be returned to the pool and can be used to do something else in the meantime. When the i/o completes then another thread from the pool can take over and complete that waiting, which can then finish the waiting in a task waiting on that, and so on.

As such the first example in your question allows for fewer threads being used in total, especially when other work will also being using threads from the same pool.

In the second example once the first await has completed the thread that handled its completion will be blocking on the synchronous equivalent methods. If other operations also need to use threads from the pool then that thread not being returned to it, fresh threads will have to be spun up. As such the second example is the example that will need more threads.

Jon Hanna
  • 110,372
  • 10
  • 146
  • 251
  • Ok. Let's ignore threads for this, as it's initiation is optmized :) So in my datareader example, when the async while loop finishes, the task is complete? Or does it carry on? And then when the `NextResultAsync` is hit that creates a new task? And further `ReadAsync` will keep on spinning up new tasks? – Umair Apr 17 '16 at 15:04
  • "spinning up a new task" makes it sound similar to spinning up a new thread. If you looped through `ReadAsync` a hundred times then it would have been a hundred tasks and then another one on next task. That though is a hundred times a thread was potentially made available for other work when it could instead have been blocking. While it is worth avoiding allocating tasks if you can (e.g. using `Task.CompletedTask()` when applicable) just as we optimise by avoiding allocations generally, tasks are generally cheaper than blocking threads, so that's 101 times you've made a saving. – Jon Hanna Apr 17 '16 at 15:19
  • (It's worth noting that the work to avoid allocating tasks by reusing task objects is often done within the framework, so you already have that advantage sometimes. Also take a look at `ValueTask` that's currently in CoreFX for awaitable objects that avoid task allocation more often). – Jon Hanna Apr 17 '16 at 15:22
0

One is not better than the other, they do different things.

In the first example, each operation is scheduled and performed on a thread, represented by a Task. Note: It's not guaranteed what thread they happen on.

The await keyword means (loosely) "wait until this asynchronous operation has finished and then continue". The continuation, is not necessarily done on the same thread either.

This means example one, is a synchronous processing of asynchronous operations. Now just because a Task is created, it doesn't infer a Thread is also created, there is a pool of threads the TaskScheduler uses, which have already been created, very minimal overhead is actually introduced.

In your second example, the await will call the first operation using the scheduler, then call the next two as normal. No Task or Thread is created for the second two calls, nor is it calling methods on a Task.

In the first example, you can also look into making your asynchronous calls simultaneous. Which will schedule all three operations to run "at the same time" (not guaranteed), and wait until they have all finished executing.

public async Task DoWork()
{
    var o1 = this.Operation1Async();
    var o2 = this.Operation2Async();
    var o3 = this.Operation3Async();

    await Task.WhenAll(o1, o2, o3);
}
Xenolightning
  • 4,140
  • 1
  • 26
  • 34
  • See my amendment please! – Umair Apr 17 '16 at 14:31
  • @Umair You're misunderstanding how a `Task` works. It's a unit of work. Tasks are **not** created once and then re-used for sequential method invocations. Generally a `Task` is used for short CPU bound work, or work that utilises IO completion ports – Xenolightning Apr 17 '16 at 23:54