Reputation: 3243
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
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.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
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
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
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