What would be the difference between using the await keyword vs. the Task.Wait() method on a CPU bound task?

11,997

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.

Share:
11,997
Water Cooler v2
Author by

Water Cooler v2

https://sathyaish.net/?c=pros https://www.youtube.com/user/Sathyaish

Updated on June 05, 2022

Comments

  • Water Cooler v2
    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 the CPUBoundWork 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 which Wait 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 calling Wait.

    Of course, all of this is mere speculation. I want someone to confirm it.

  • Water Cooler v2
    Water Cooler v2 almost 8 years
    I have updated the question with a theory I have. It would be nice if someone could confirm or invalidate my assumptions.
  • Water Cooler v2
    Water Cooler v2 almost 8 years
    Thank you very much for your reply. I will study more and come back to re-read your answer.
  • Water Cooler v2
    Water Cooler v2 almost 8 years
    Thank you very much for your reply. I will study more and come back to re-read your answer.
  • Ben Voigt
    Ben Voigt almost 8 years
    You 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
    Water Cooler v2 almost 8 years
    Thank 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
    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
    Water Cooler v2 almost 8 years
    Or 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
    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, an async method may return a task at any await or, ultimately, on return or throw, returning an already completed task.
  • Water Cooler v2
    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
    lvoros almost 8 years
    Yes, you're right. I've corrected it. Thank you for the explanation.
  • Water Cooler v2
    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
    bmvr about 7 years
    @StephenCleary if I place in my code an await MyTask() and right in the next line a await MyTask2() my second task (MyTask2()) will only be called after MyTask() finish, right? The only thing is that by using await instead of wait, every processes running in the same thread, will not be affected(paused) because I did not block the curr. thread, right?
  • Stephen Cleary
    Stephen Cleary about 7 years
    Yes. See my async intro for more info.