What would be the difference between using the await keyword vs. the Task.Wait() method on a CPU bound task?
Solution 1
It is my understanding that await disassociates any affinity between the thread and the task that was being run on the thread.
No, that's not at all what happens.
First, await
causes nothing to run, nothing to be "scheduled". The scheduling (if any) and running (if any) are already in progress before you get to the await
.
await
is an "asynchronous wait"; that is, it asynchronously waits for the task to complete. "Asynchronous" here means "without blocking the current thread".
I have a more detailed introduction to async
and await
on my blog.
I understand that logically, both would result in the same flow-of-control.
Not really. The Wait
will block the current thread, while the await
will not.
Under both the cases, the DoIOAsync method would get called only after the CPUBoundWork task has completed execution.
This is correct.
However, would there be a difference in terms of the scheduling of the CPU task in both the cases?
No, in both cases the scheduling is done by Task.Run
, not Wait
or await
.
While this works great for I/O requests, since you now re-use an I/O thread that was blocked on a network driver, you want to maintain thread-affinity for CPU bound work.
If you want to stay on the same thread, then you can't use Task.Run
. You'd have to just call CPUBoundWork
directly.
the Wait() method, I am not sure but purely guessing, simply waits for the task to complete. If the task has not yet begun, it executes it on the current thread.
There's a bit more to it than that. Sometimes it will, sometimes it won't. In the code you posted, it usually won't (because CPUBoundWork
will have already started).
Therefore, thread-affinity for the task is maintained when calling Wait.
Again, this is a simplification. If by "thread affinity" you mean that the thread is the same before and after the Wait
, then yes, that's correct.
However, blocking on tasks within an asynchronous method can be dangerous; as I describe on my blog, you can get into deadlock situations. If you're considering using Task.Run
in conjunction with async
, please review my Task.Run
etiquette guide.
Solution 2
An easy way to understand what is happening, if you "convert" the async..await structure to Task and ContinueWith.
What I mean? Your first code:
async void LongIOBoundWorkWithSomeCPUBoundWorkAsWellAsync()
{
await Task.Run(CPUBoundWork);
// Do IO bound work
await DoIOAsync();
}
will be converted to:
Task LongIOBoundWorkWithSomeCPUBoundWorkAsWellAsync()
{
return Task.Run(CPUBoundWork)
.ContinueWith(t => {
// Do IO bound work
DoIOAsync();
}).Unwrap();
}
so it returns immediately to the caller with a Task-chain.
Your second code:
async void LongIOBoundWorkWithSomeCPUBoundWorkAsWellAsync()
{
var cpuTask = Task.Run(CPUBoundWork);
cpuTask.Wait();
// Do IO bound work
await DoIOAsync();
}
will be converted to:
Task LongIOBoundWorkWithSomeCPUBoundWorkAsWellAsync()
{
var cpuTask = Task.Run(CPUBoundWork);
cpuTask.Wait();
// Do IO bound work
return DoIOAsync();
}
so it waits until the first Task is ready, and then returns to the caller with the Task of DoIOAsync.
So the two approaches are definitve not the same! The second code will block the caller until CPUBoundWork is ready.
Solution 3
As far as scheduling goes, there won't really be a difference between the contexts once you reach DoIOAsync
, other than the first one could possibly be on a different thread.
The main difference here is that Wait()
blocks a thread, while await
allows that thread to be re-used. Blocking a thread is way more expensive than switching to a new task. You're also potentially putting more pressure on the thread pool to grow.
Of course, that's all if you reach DoIOAsync
. Mixing async and sync is a recipe for deadlocks and generally not recommended if you're not absolutely sure what's going on.
Water Cooler v2
https://sathyaish.net/?c=pros https://www.youtube.com/user/Sathyaish
Updated on June 05, 2022Comments
-
Water Cooler v2 almost 2 years
What would be the mechanical difference between?
async void LongIOBoundWorkWithSomeCPUBoundWorkAsWellAsync() { await Task.Run(CPUBoundWork); // Do IO bound work await DoIOAsync(); } and async void LongIOBoundWorkWithSomeCPUBoundWorkAsWellAsync() { var cpuTask = Task.Run(CPUBoundWork); cpuTask.Wait(); // Do IO bound work await DoIOAsync(); }
I understand that logically, both would result in the same flow-of-control. Under both the cases, the
DoIOAsync
method would get called only after theCPUBoundWork
task has completed execution.However, would there be a difference in terms of the scheduling of the CPU task in both the cases?
Update
Please confirm if my uneducated rumination about the code above is correct.
It is my understanding that
await
disassociates any affinity between the thread and the task that was being run on the thread. While this works great for I/O requests, since you now re-use an I/O thread that was blocked on a network driver, you want to maintain thread-affinity for CPU bound work.While await destroys this affinity, the
Wait()
method, I am not sure but purely guessing, simply waits for the task to complete. If the task has not yet begun, it executes it on the current thread. If, however, the task had already begun earlier, it blocks the current thread, putting the current thread on the wait queue, the next time the thread that was executing the task on whichWait
is called comes around and finishes its work, it signals the waiting thread and the waiting thread continues. Therefore, thread-affinity for the task is maintained when callingWait
.Of course, all of this is mere speculation. I want someone to confirm it.
-
Water Cooler v2 almost 8 yearsI have updated the question with a theory I have. It would be nice if someone could confirm or invalidate my assumptions.
-
Water Cooler v2 almost 8 yearsThank you very much for your reply. I will study more and come back to re-read your answer.
-
Water Cooler v2 almost 8 yearsThank you very much for your reply. I will study more and come back to re-read your answer.
-
Ben Voigt almost 8 yearsYou kinda glossed over the question statement that "you now re-use an I/O thread that was blocked on a network driver"... probably good to recap "There is no thread" (for in-flight overlapped I/O)
-
Water Cooler v2 almost 8 yearsThank you. Thing is: I know all this and wrote exactly all this in my Update section of the question. When I said logical flow-of-control, I did not mean what will happen internally. Instead, I meant in what sequence what method will be executed. And what you all are explaining is what I meant by mechanical difference. And what everyone, including Stephen Clearey is saying in his answer is precisely what I wrote in my Update section in the question above, albeit in different words. It's a difference of words here, not of opinions.
-
Water Cooler v2 almost 8 years@StephenCleary: Actually. what I wrote in the Updated section is precisely what you are saying. I think it is a matter of using different words. I should have clarified further. I will come back to add more to this question and ask what I am trying to ask after reading all the documentation and the articles you and others link to.
-
Water Cooler v2 almost 8 yearsOr may be what you are saying is different but at this time, to my uninformed mind, it looks like what I am saying is what you are saying in different words. At any rate, I will need to come back to this question after some more study on this subject.
-
acelent almost 8 years@lvoros, you need
.Unwrap()
after.ContinueWith(...)
in the first example to have the same effect. Moreover,await
does more than that: it checks if the task has already completed, and if so, continues execution. So, in essence, anasync
method may return a task at anyawait
or, ultimately, on return or throw, returning an already completed task. -
Water Cooler v2 almost 8 years@StephenCleary: I read your philosophical account titled, "There is no thread!" and I believe you're saying what I was saying but in very different words, and with much detail and clarity as to what happens in the OS and the device driver. I liked your explanation and found that article to be very useful. Thank you very much.
-
lvoros almost 8 yearsYes, you're right. I've corrected it. Thank you for the explanation.
-
Water Cooler v2 almost 8 years@StephenClearly I understand a little more now what
await
does after having reproduced the state machine it produces, by hand. So, my assumption that await disassociates thread affinity is not correct, as you seem to rightly indicate in your answer. Thanks much. -
bmvr about 7 years@StephenCleary if I place in my code an
await MyTask()
and right in the next line aawait MyTask2()
my second task (MyTask2()) will only be called after MyTask() finish, right? The only thing is that by usingawait
instead ofwait
, every processes running in the same thread, will not be affected(paused) because I did not block the curr. thread, right? -
Stephen Cleary about 7 yearsYes. See my async intro for more info.