3

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 creatE 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://learn.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 explain what's exactly happening when awaiting a Task ?

Soufien Hajji
  • 477
  • 1
  • 8
  • 24
  • Task.Run will use the threadpool, so it will most likely run on *a* different thread, but it doesn't necessarily mean that it will always create a new thread just for your task. – Lasse V. Karlsen Feb 19 '19 at 10:10
  • Okay, what about Task.WhenAll ? will it use different multiple threads in the threadpool ? Do you also have an answer to my last question ? – Soufien Hajji Feb 19 '19 at 10:13
  • The whole point of tasks is to allow you to run something in the background, therefore not blocking the UI. There are other things you can do with them, such as running multiple tasks in parallel, but generally speaking don't think of tasks as anything to do with threads (even though they are). – Reinstate Monica Cellio Feb 19 '19 at 10:14
  • I cant answer this question any better than this: https://stackoverflow.com/a/37419845/2648798 – Robert Perry Feb 19 '19 at 10:14
  • 2
    Possible duplicate of [If async-await doesn't create any additional threads, then how does it make applications responsive?](https://stackoverflow.com/questions/37419572/if-async-await-doesnt-create-any-additional-threads-then-how-does-it-make-appl) – Robert Perry Feb 19 '19 at 10:15
  • @Archer Running to me something in background is equivalent to running something in another thread than the main thread. But this is not true according to some articles because it is said that using async await will execute on the main thread – Soufien Hajji Feb 19 '19 at 10:19
  • 3
    read [Stephen Cleary There is no thread](http://blog.stephencleary.com/2013/11/there-is-no-thread.html) you'll find it is referenced in almost every good answer in SO regarding async/await – Brett Caswell Feb 19 '19 at 10:20
  • @SoufienHajji You're just talking semantics. It is what it is, regardless of what specific words you use to explain it. See the link from RobertPerry above - it explains it well. – Reinstate Monica Cellio Feb 19 '19 at 10:20
  • As an aside, it's worth pointing out that although `HttpClient` implements `IDisposable`, you shouldn't really be instantiating new instances for every request. See https://stackoverflow.com/q/15705092/77090 – Graham Clark Feb 19 '19 at 18:40

4 Answers4

22

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.

Eric Lippert
  • 647,829
  • 179
  • 1,238
  • 2,067
  • I appreciate your great answer, thank you very much. I just want to confirm something. Is it always the case that there's no thread created when awaiting a task. 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. 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 ? – Soufien Hajji Feb 20 '19 at 08:50
  • @SoufienHajji: You're welcome. I've addressed your follow-up question in the answer. – Eric Lippert Feb 20 '19 at 15:59
  • 3
    The joy of building a "multithreaded" UI using old line-characters on the IBM/PC DOS using TurboPascal and faking it all by saving the registers and jumping to the next "thread" by restoring registers from memory, including the instruction pointer. Brings back memories. As always, Eric Lippert kills it. – Lasse V. Karlsen Feb 20 '19 at 22:56
  • @LasseVågsætherKarlsen: When I was a co-op student I wrote -- as *production code that shipped to customers* -- an implementation of fibers on top of the character-mode NetWare server OS. There was some weird thing you had to do to safely move the stack register but I cannot for the life of me remember what it was. I look back on that and I am half astonished and half horrified that they shipped my hacked-up implementation to real customers who depended on it. Netware ran user programs in the zero ring! Any mistake could wipe out any memory. – Eric Lippert Feb 20 '19 at 23:10
  • @EricLippert Probably your answer is one of the best answers i've ever had, and the music joke was hilarious. Thank you very much ^_^ i couldnt ask for more :) – Soufien Hajji Feb 21 '19 at 09:09
3

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

This is correct. By themselves, async and await do not directly use threads. Their purpose is to free up the calling thread.

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

No; neither Task.WhenAll nor Task.WhenAny directly use any threads.

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

No. GetStringAsync is invoked synchronously on the current thread, just like any other method. It returns an incomplete task, again, synchronously.

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.

Close, except that there's no other thread. await getStringTask will check to see if the task is completed; if it's not, then it will return an incomplete task from AccessTheWebAsync.

Can someone exmplain what exactly happening when awaiting a Task ?

I recommend reading my async intro for more details.

Stephen Cleary
  • 437,863
  • 77
  • 675
  • 810
  • Thanks. I just finished reading your [article](http://blog.stephencleary.com/2013/11/there-is-no-thread.html) I learn that only Task.Run will look for available threads in the thread pool to do CPU bound code. As for I/O bound code it will be executed on low level CPU components and not threads and only use a thread briefly to notify the context about the finished Task status. – Soufien Hajji Feb 19 '19 at 15:01
0

Your basic assumption — a Task always runs on a thread — is indeed wrong. A simple counterexample would be a timer-based task which doesn't run at all: it just subscribes on timer and sets task state to completed whenever the timer fires.

More useful and more practical example of the tasks not running anywhere — network requests: they send the request, subscribe to the incoming answer and just stop running, freeing thread for another work*.

So let's consider your actual questions.


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

No, Task.WhenAll won't create any new threads. It will wait for the already existing tasks to complete regardless of where they run (and regardless of if they run in any thread at all!).

The task created by Task.WhenAll is not running in any specific thread itself! It just detects when the underlying tasks are finished, and after all of them are ready, finishes itself too. Task.WhenAll doesn't need any thread for doing this.


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

Calling an async method like GetStringAsync, as we seen before, won't be executed on any specific thread. The code of GetStringAsync sets up the things so that it gets control back (perhaps on a thread pool thread) when the answer comes, and yields control back to you. The preparation work can be perfectly done on current thread, it doesn't take too much time*.


*Disclaimer: it's a simplification, indeed the sequence of actions done by a network async request is much more complicated.

Vlad
  • 35,022
  • 6
  • 77
  • 199
  • Thanks for your answer. For my first question, let's say we have Task.WhenAll( 5 Tasks). Provided that all 5 Tasks don't need any synchronisation, these task will run in parallel, does that mean that there will be 5 threads used in the same time in our thread pool (If we find 5 threads directly available of course) ? – Soufien Hajji Feb 19 '19 at 12:39
  • 1
    @SoufienHajji: Let me reiterate that the tasks are not running on a thread. So 1000 running tasks doesn't mean 1000 threads taken. – Vlad Feb 19 '19 at 13:00
0

An article that helped me a lot to understand async-await is this interview with Eric Lippert, where he compares async-await with a cook making breakfast. Search somewhere in the middle for async-await.

If a cook has to make breakfast and he just put some bread in the toaster, he doesn't wait idly for the bread to be toasted, but starts looking around to see if he can do something else, for instance boiling water for the tea.

Something similar happens when you see async-await. If you call an async function, you know that somewhere inside is an await. In fact, your compiler will warn you if you forget to await in your async function.

Once your thread sees the await, it does not wait idly for the awaitable task to finish, but it looks around to see if it can do other things. It can go up the call stack to see if one of the callers is not awaiting yet and execute those statements until it sees an await. Go up the call stack again and execute statements until you see an await.

There is no guarantee that the thread that continues the statements after your not-awaited async call is the same as your original thread. But because this thread has the same "context" you can act as if it is the same thread. There is no need for a critical section and the like.

Console.Writeline(Thread.CurrentThread.ManagedThreadId);

// async call to the text reader to read a line; don't await
var taskReadLine = myTextReader.ReadLineAsync()

// because I did not await, the following will be executed as soon as a thread is free
Console.Writeline(Thread.CurrentThread.ManagedThreadId);
...

// we need the read line; await for it
string readLine = await taskReadLine;
Console.Writeline(Thread.CurrentThread.ManagedThreadId);
ProcessReadLine(readLine);

There is no guarantee that the thread that does the DoSomething is the same thread that was used to call the ReadLineAsync. If you execute the code in a simple test program, chances are high that you get more than one thread id.

Your code should not depend on any statement within the async function to be executed before you await the result:

async Task<int> DoIt()
{
    this.X = 4;
    await DoSomethingElseAsync(this.X);
    return 5;
}
async Task CallDoItAsync()
{
    this.X = 0;
    var taskDoIt = DoIt();

    // you didn't await, it is not guaranteed that this.X already changed to 4
    ...
    int i = await taskDoIt();
    // now you can be certain that at some moment 4 had been assigned to this.X 

Creating a Task object does not create a thread. Creating a thread is fairly expensive. Therefore your process has a thread pool, containing several threads. Threads that are idle are put in the pool and are available to do other things upon request. As soon as your process needs a thread it takes an available thread from the thread pool and schedules it for running.

I'm not sure what happens if no thread if available in the pool. I guess that your function simply has to wait for an available thread.

You can access the thread pool using the static ThreadPool class.

ThreadPool.GetMaxThreads (out int workerThreads, out int completionPortThreads);
++workerThreads;
++completionPortThreads;
bool success = ThreadPool.SetMaxThreads (workerThreads, completionPortThreads);

Be very careful changing the thread pool!

Some people say that async-await is only useful to keep the UI responsive, but the following shows that it also can improve processing speed.

Non-async:

void CopyFile(FileInfo infile, FileInfo outFile)
{
     using(var textReader = inFile.OpenText())
     {
        using (var textWriter = outFile.CreateText())
        {
            // Read a line. Wait until line read
            var line = textReader.ReadLine();
            while (line != null)
            {
                // Write the line. Wait until line written
                textWrite.WriteLine(line);

                // Read the next line. Wait until line read
                line = textReader.ReadLine();
            }
        }
    }
}

You see all the waits. Luckily a TextReader and TextWriter do buffer data, otherwise we really had to wait until the data was written before the next line was read

async Task CopyFileAsync(FileInfo infile, FileInfo outFile)
{
     using(var textReader = inFile.OpenText())
     {
        using (var textWriter = outFile.CreateText())
        {
            // Read a line. Wait until line read
            var line = await textReader.ReadLineAsync();
            while (line != null)
            {
                // Write the line. Don't wait until line written
                var writeTask = textWrite.WriteLineAsync(line);

                // While the line is being written, I'm free to read the next line. 
                line = textReader.ReadLine();

                // await until the previous line has been written:
                await writeTask;
            }
        }
    }
}

While a line is being written, we already try to read the next line. This can improve processing speed.

Harald Coppoolse
  • 28,834
  • 7
  • 67
  • 116