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.
I just want to confirm something. Is it always the case that there's no thread created when awaiting a task?
We worried when designing the feature that people would believe, as you still might, that "await" does something to the call which comes after it. It does not. Await does something to the return value. Again, when you see:
int foo = await FooAsync();
you should mentally see:
Task<int> task = FooAsync();
if (task is not already completed)
set continuation of task to go to "resume" on completion
return;
resume: // If we get here, task is completed
int foo = task.Result;
A call to a method with an await is not a special kind of call. The "await" does not spin up a thread, or anything like that. It is an operator that operates on the value that was returned.
So awaiting a task does not spin up a thread. Awaiting a task (1) checks to see if the task is complete, and (2) if it is not, assigns the remainder of the method as the continuation of the task, and returns. That's all. Await does not do anything to create a thread. Now, maybe the called method spins up a thread; that's it's business. That has nothing to do with the await, because the await doesn't happen until after the call returns. The called function does not know its return value is being awaited.
Let's say we await a CPU bound task that does heavy calculations. What i know so far is a I/O bound code it will be executed on low level CPU components (much lower than threads) and only use a thread briefly to notify the context about the finished Task status.
What we know about the call to FooAsync above is that it is asynchronous, and it returns a task. We do not know how it is asynchronous. That's the author of FooAsync's business! But there are three main techniques that the author of FooAsync can use to achieve asynchrony. As you note, the two main techniques are:
If the task is high-latency because it requires a long computation to be done on the current machine on another CPU, then it makes sense to obtain a worker thread and start the thread doing the work on another CPU. When the work is finished, the associated task can schedule its continuation to run back on the UI thread, if the task was created on the UI thread, or on another worker thread, as appropriate.
If the task is high-latency because it requires communication with slow hardware, like disks or networks, then as you note, there is no thread. Special-purpose hardware does the task asynchronously and the interrupt handling provided by the operating system ultimately takes care of getting the task completion scheduled on the right thread.
A third reason to be asynchronous is not because you're managing a high-latency operation, but because you're breaking up an algorithm into little parts and putting them on a work queue. Maybe you're making your own custom scheduler, or implementing an actor model system, or trying to do stackless programming, or whatever. There's no thread, there's no IO, but there is asynchrony.
So, again, awaiting does not make something run on a worker thread. Calling a method that starts a worker thread makes something run on a worker thread. Let the method you're calling decide whether to make a worker thread or not. Async methods are already asynchronous. You don't need to do anything to them to make them asynchronous. Await does not make anything asynchronous.
Await exists solely to make it easier for the developer to check whether an asynchronous operation has completed, and to sign up the remainder of the current method as the continuation if it has not completed. That's what it is for. Again, await does not create asynchrony. Await helps you build asynchronous workflows. An await is a point in the workflow where an asynchronous task must be completed before the workflow can continue.
I also know that we use Task.Run() to execute CPU bound code to look for an available thread in thread pool. Is this true ?
That's correct. If you have a synchronous method, and you know that it is CPU bound, and you would like it to be asynchronous, and you know that the method is safe to run on another thread, then Task.Run will find a worker thread, schedule the delegate to be executed on the worker thread, and give you a task representing the asynchronous operation. You should only do this with methods that are (1) very long-running, like, more than 30 milliseconds, (2) CPU bound, (3) safe to call on another thread.
If you violate any of those, bad things happen. If you hire a worker to do less than 30 milliseconds of work, well, think about real life. If you have some computations to do, does it make sense to buy an ad, interview candidates, hire someone, get them to add three dozen numbers together, and then fire them? Hiring a worker thread is expensive. If hiring the thread is more expensive than just doing the work yourself, you will not get any performance win at all by hiring a thread; you'll make it a lot worse.
If you hire a worker to do IO bound tasks, what you've done is hired a worker to sit by the mailbox for years and yell when mail arrives. That does not make the mail arrive faster. It just wastes worker resources that could be spent on other problems.
And if you hire a worker to do a task that is not threadsafe, well, if you hire two workers and tell them to both drive the same car to two different locations at the same time, they're going to crash the car while they're fighting over the steering wheel on the freeway.