What does awaiting an asynchronous method do in background?

  • A+
Category:Languages

I've red various articles about async await and i'm trying to understand the await async in depth. My problem is that i found out that awaiting an asyncronous method doesn't creat a new thread, it rather just make the UI responsive. If it's like that there's no time gain when using await async since no extra thread is used.

What i knew so far is that only Task.Run() create a new thread. Is this also true for Task.WhenAll() or Task.WhenAny() ?

Let's say we have this code :

    async Task<int> AccessTheWebAsync()             {                 using (HttpClient client = new HttpClient())                 {                     Task<string> getStringTask = client.GetStringAsync("https://docs.microsoft.com");                      DoIndependentWork();                      string urlContents = await getStringTask;                      return urlContents.Length;                 }             } 

What i expect :

  1. When creating the getStringTask Task, another thread will copy the current context and start executing the GetStringAsync method.

  2. When awaiting getStringTask, we will see if the other thread has completed his task, if not the control will be back the caller of AccessTheWebAsync() method until the other thread complets it's task to resume the control.

So i really don't get how no extra thread is created when awaiting a Task. Can someone exmplain what exactly happening when awaiting a Task ?

 


I've red various articles about async await and i'm trying to understand the await async in depth.

A noble pursuit.

My problem is that i found out that awaiting an asyncronous method doesn't creat a new thread, it rather just make the UI responsive.

Correct. It is very important to realize that await means asynchronous wait. It does not mean "make this operation asynchronous". It means:

  • This operation is already asynchronous.
  • If the operation is complete, fetch its result
  • If the operation is not complete, return to the caller and assign the remainder of this workflow as the continuation of the incomplete operation.
  • When the incomplete operation becomes complete, it will schedule the continuation to execute.

If it's like that there's no time gain when using await async since no extra thread is used.

This is incorrect. You're not thinking about the time win correctly.

Imagine this scenario.

  • Imagine a world with no ATMs. I grew up in that world. It was a strange time. So there is usually a line of people at the bank waiting to deposit or withdraw money.
  • Imagine there is only one teller at this bank.
  • Now imagine that the bank only takes and gives out single dollar bills.

Suppose there are three people in line and they each want ten dollars. You join the end of the line, and you only want one dollar. Here are two algorithms:

  • Give the first person in the line one dollar.
  • [ do that ten times ]
  • Give the second person in the line one dollar.
  • [ do that ten times ]
  • Give the third person in the line one dollar.
  • [ do that ten times ]
  • Give you your dollar.

How long does everyone have to wait to get all their money?

  • Person one waits 10 time units
  • Person two waits 20
  • Person three waits 30
  • You wait 31.

That's a synchronous algorithm. An asynchronous algorithm is:

  • Give the first person in the line one dollar.
  • Give the second person in the line one dollar.
  • Give the third person in the line one dollar.
  • Give you your dollar.
  • Give the first person in the line one dollar.
  • ...

That's an asynchronous solution. Now how long does everyone wait?

  • Everyone getting ten dollars waits about 30.
  • You wait 4 units.

The average throughput for large jobs is lower, but the average throughput for small jobs is much higher. That's the win. Also, the time-to-first-dollar for everyone is lower in the asynchronous workflow, even if the time to last dollar is higher for big jobs. Also, the asynchronous system is fair; every job waits approximately (size of job)x(number of jobs). In the synchronous system, some jobs wait almost no time and some wait a really long time.

The other win is: tellers are expensive; this system hires a single teller and gets good throughput for small jobs. To get good throughput in the synchronous system, as you note, you need to hire more tellers which is expensive.

Is this also true for Task.WhenAll() or Task.WhenAny() ?

They do not create threads. They just take a bunch of tasks and complete when all/any of the tasks are done.

When creating the getStringTask Task, another thread will copy the current context and start executing the GetStringAsync method.

Absolutely not. The task is already asynchronous and since it is an IO task it doesn't need a thread. The IO hardware is already asynchronous. There is no new worker hired.

When awaiting getStringTask, we will see if the other thread has completed his task

No, there is no other thread. We see if the IO hardware has completed its task. There is no thread.

When you put a piece of bread in the toaster, and then go check your email, there is no person in the toaster running the toaster. The fact that you can start an asynchronous job and then go off and do other stuff while it is working is because you have special purpose hardware that is by its nature asynchronous. That's true of network hardware the same way it is true of toasters. There is no thread. There is no tiny person running your toaster. It runs itself.

if not the control will be back to the caller of AccessTheWebAsync() method until the other thread completes its task to resume the control.

Again, there is no other thread.

But the control flow is correct. If the task is complete then the value of the task is fetched. If it is not complete then control returns to the caller, after assigning the remainder of the current workflow as the continuation of the task. When the task is complete, the continuation is scheduled to run.

i really don't get how no extra thread is created when awaiting a Task.

Again, think about every time in your life when you stopped doing a task because you were blocked, did something else for a while, and then started up doing the first task again when you got unblocked. Did you have to hire a worker? Of course not. Yet somehow you managed to make eggs while the toast was in the toaster. Task based asynchrony just puts that real-world workflow into software.

It never ceases to amaze me how you kids today with your weird music act like threads always existed and there is no other way to do multitasking. I learned how to program in an operating system that didn't have threads. If you wanted two things to appear to happen at the same time, you had to build your own asynchrony; it wasn't built into the language or the OS. Yet we managed.

Cooperative single-threaded asynchrony is a return to the world as it was before we made the mistake of introducing threads as a control flow structure; a more elegant and far simpler world. An await is a suspension point in a cooperative multitasking system. In pre-threading Windows, you'd call Yield() for that, and we didn't have language support for creating continuations and closures; you wanted state to persist across a yield, you wrote the code to do it. You all have it easy!

Can someone explain what exactly happening when awaiting a Task ?

Exactly what you said, just with no thread. Check to see if the task is done; if it's done, you're done. If not, schedule the remainder of the workflow as the continuation of the task, and return. That's all await does.

Comment

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen: