Umair
Umair

Reputation: 3243

Multiple await operations or just one

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();

// ...

Upvotes: 4

Views: 1959

Answers (4)

Stephen Cleary
Stephen Cleary

Reputation: 456657

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.

Upvotes: 5

Jon Hanna
Jon Hanna

Reputation: 113292

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.

Upvotes: 1

xenolightning
xenolightning

Reputation: 4230

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);
}

Upvotes: 0

Darin Dimitrov
Darin Dimitrov

Reputation: 1038930

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.

Upvotes: 2

Related Questions