Does the Use of Async/Await Create a New Thread

Does the use of async/await create a new thread?

In short NO

From Asynchronous Programming with Async and Await : Threads

The async and await keywords don't cause additional threads to be
created. Async methods don't require multithreading because an async
method doesn't run on its own thread. The method runs on the current
synchronization context and uses time on the thread only when the
method is active. You can use Task.Run to move CPU-bound work to a
background thread, but a background thread doesn't help with a process
that's just waiting for results to become available.

Does async run in a separate thread?

No, it does not. It MAY start another thread internally and return that task, but the general idea is that it does not run on any thread.

Let me explain. The general use of async is if you are not CPU bound and that means IO and all IO in windows has callback operated interfaces at the lowest level, so - a network request sends the request and then goes on with work - but there is no thread attached. At all. The general use case for async is that async runs on a thread and when nothing is there to do it will then use the threads for the tasks completed, allowing multiple operations on one thread - AND... the IO will not use up a thread.

Your method basically turns into a state engine relinquishing control to the task scheduler waiting for a completed task being signalled.

If async-await doesn't create any additional threads, then how does it make applications responsive?

Actually, async/await is not that magical. The full topic is quite broad but for a quick yet complete enough answer to your question I think we can manage.

Let's tackle a simple button click event in a Windows Forms application:

public async void button1_Click(object sender, EventArgs e)
{
Console.WriteLine("before awaiting");
await GetSomethingAsync();
Console.WriteLine("after awaiting");
}

I'm going to explicitly not talk about whatever it is GetSomethingAsync is returning for now. Let's just say this is something that will complete after, say, 2 seconds.

In a traditional, non-asynchronous, world, your button click event handler would look something like this:

public void button1_Click(object sender, EventArgs e)
{
Console.WriteLine("before waiting");
DoSomethingThatTakes2Seconds();
Console.WriteLine("after waiting");
}

When you click the button in the form, the application will appear to freeze for around 2 seconds, while we wait for this method to complete. What happens is that the "message pump", basically a loop, is blocked.

This loop continuously asks windows "Has anyone done something, like moved the mouse, clicked on something? Do I need to repaint something? If so, tell me!" and then processes that "something". This loop got a message that the user clicked on "button1" (or the equivalent type of message from Windows), and ended up calling our button1_Click method above. Until this method returns, this loop is now stuck waiting. This takes 2 seconds and during this, no messages are being processed.

Most things that deal with windows are done using messages, which means that if the message loop stops pumping messages, even for just a second, it is quickly noticeable by the user. For instance, if you move notepad or any other program on top of your own program, and then away again, a flurry of paint messages are sent to your program indicating which region of the window that now suddenly became visible again. If the message loop that processes these messages is waiting for something, blocked, then no painting is done.

So, if in the first example, async/await doesn't create new threads, how does it do it?

Well, what happens is that your method is split into two. This is one of those broad topic type of things so I won't go into too much detail but suffice to say the method is split into these two things:

  1. All the code leading up to await, including the call to GetSomethingAsync
  2. All the code following await

Illustration:

code... code... code... await X(); ... code... code... code...

Rearranged:

code... code... code... var x = X(); await X; code... code... code...
^ ^ ^ ^
+---- portion 1 -------------------+ +---- portion 2 ------+

Basically the method executes like this:

  1. It executes everything up to await

  2. It calls the GetSomethingAsync method, which does its thing, and returns something that will complete 2 seconds in the future

    So far we're still inside the original call to button1_Click, happening on the main thread, called from the message loop. If the code leading up to await takes a lot of time, the UI will still freeze. In our example, not so much

  3. What the await keyword, together with some clever compiler magic, does is that it basically something like "Ok, you know what, I'm going to simply return from the button click event handler here. When you (as in, the thing we're waiting for) get around to completing, let me know because I still have some code left to execute".

    Actually it will let the SynchronizationContext class know that it is done, which, depending on the actual synchronization context that is in play right now, will queue up for execution. The context class used in a Windows Forms program will queue it using the queue that the message loop is pumping.

  4. So it returns back to the message loop, which is now free to continue pumping messages, like moving the window, resizing it, or clicking other buttons.

    For the user, the UI is now responsive again, processing other button clicks, resizing and most importantly, redrawing, so it doesn't appear to freeze.

  5. 2 seconds later, the thing we're waiting for completes and what happens now is that it (well, the synchronization context) places a message into the queue that the message loop is looking at, saying "Hey, I got some more code for you to execute", and this code is all the code after the await.

  6. When the message loop gets to that message, it will basically "re-enter" that method where it left off, just after await and continue executing the rest of the method. Note that this code is again called from the message loop so if this code happens to do something lengthy without using async/await properly, it will again block the message loop

There are many moving parts under the hood here so here are some links to more information, I was going to say "should you need it", but this topic is quite broad and it is fairly important to know some of those moving parts. Invariably you're going to understand that async/await is still a leaky concept. Some of the underlying limitations and problems still leak up into the surrounding code, and if they don't, you usually end up having to debug an application that breaks randomly for seemingly no good reason.

  • Asynchronous Programming with Async and Await (C# and Visual Basic)
  • SynchronizationContext Class
  • Stephen Cleary - There is no thread well worth a read!
  • Channel 9 - Mads Torgersen: Inside C# Async well worth a watch!

OK, so what if GetSomethingAsync spins up a thread that will complete in 2 seconds? Yes, then obviously there is a new thread in play. This thread, however, is not because of the async-ness of this method, it is because the programmer of this method chose a thread to implement asynchronous code. Almost all asynchronous I/O don't use a thread, they use different things. async/await by themselves do not spin up new threads but obviously the "things we wait for" may be implemented using threads.

There are many things in .NET that do not necessarily spin up a thread on their own but are still asynchronous:

  • Web requests (and many other network related things that takes time)
  • Asynchronous file reading and writing
  • and many more, a good sign is if the class/interface in question has methods named SomethingSomethingAsync or BeginSomething and EndSomething and there's an IAsyncResult involved.

Usually these things do not use a thread under the hood.


OK, so you want some of that "broad topic stuff"?

Well, let's ask Try Roslyn about our button click:

Try Roslyn

I'm not going to link in the full generated class here but it's pretty gory stuff.

C# Async without creating a new thread

On first question - async/await do not create threads by themselves, which is very different from what you expect this claim to be - threads never created when code uses async/await.

In your particular case it's timer code that schedules return from delay is picking a threadpool thread to run the rest of the code. Note that if you would use environment which has non-empty synchronization context (like WPF in linked article) then code that continue execution after delay would come back to original thread and wait if another task/main code is running at that point.

On second point - how to ensure non-thread safe objects are not accessed from other threads: you really have to know what operation can trigger code to run on other threads. I.e. System.Timers.Timer will do that for example even if it does not look like creating threads. Using async/await in environment that guarantees that execution will continue on same (WinForms/WPF) or at least only one at a time (ASP.Net) thread can get you mostly there.

If you want stronger guarantees you can follow WinForms/WPF approach that on enforces access to methods to be on the same thread (known as main UI thread) and provide guidance to use async/await with synchronization context that ensures execution continues on the same thread it was await-ed (and also provides Invoke functionality directly for others to manage threading themselves). This would likely require wrapping classes you don't own with some "thread enforcement" proxies.

async await and threading

This is poorly advertised in the documentation, but the way that async/await works in a console application is very different than how it works in a UI application due to the lack of a synchronization context in a console application. This article describes details and gives a code sample of how add one so that async/await behaves in a more predictable way.

Normally, you're absolutely correct about async/await not necessarily entailing multithreading (although things like Task.Run do, in fact, cause the code in question to run in the thread pool), but (as described in the article I linked to) in a console application async/await could run anywhere.

My usual analogy for how async works when running on the same thread is to think of going to a restaurant with 9 other people (so 10 people total). When the waiter comes by, 9 of the people are ready and the 10th isn't. In this case, the waiter will take the other 9 people's orders first and then come back to the 10th person. If for whatever reason the 10th person's being really slow the waiter could always bring the orders back to the kitchen and come back when the 10th person's ready.

Clearly, there's no point to bringing in a second waiter just to wait for the 10th guy to be ready to order, and it would clearly be inefficient to make everyone else wait for the one guy to be ready. Adding extra waiters to the situation won't speed things up because the delay isn't being caused by a lack of waiters, it's caused by the 10th guy being slow to make up his mind (and there's nothing the wait staff can do about that).

New Thread and Async

If you need to replicate your original Thread-based behaviour, you can use Task.Factory.StartNew(... , TaskCreationOptions.LongRunning) to schedule your work, and then block until the worker tasks complete via Task.WaitAll. I do not recommended this approach, but in terms of behaviour this will be very close to how your code was working previously.

A more in-depth analysis as to why may not getting the expected performance in your scenario is as follows:

Explanation, part 1 (async does not mean "on a different thread")

Methods marked with the async keyword do not magically run asynchronously. They are merely capable of combining awaitable operations (that may or may not run asynchronously themselves), into a single larger unit (generally Task or Task<T>).

If your insert method is async, it is still likely that it performs at least some of the work synchronously. This will definitely be the case with all of your code preceding the first await statement. This work will execute on the "main" thread (thread which calls insert) - and that will be your bottleneck or at least part thereof as the degree of parallelism for that section of your code will be 1 while you're calling insert in a tight loop, regardless of whether you await the resulting task.

To illustrate the above point, consider the following example:

void Test()
{
Debug.Print($"Kicking off async chain (thread {Thread.CurrentThread.ManagedThreadId}) - this is the main thread");
OuterTask().Wait(); // Do not block on Tasks - educational purposes only.
}

async Task OuterTask()
{
Debug.Print($"OuterTask before await (thread {Thread.CurrentThread.ManagedThreadId})");
await InnerTask().ConfigureAwait(false);
Debug.Print($"OuterTask after await (thread {Thread.CurrentThread.ManagedThreadId})");
}

async Task InnerTask()
{
Debug.Print($"InnerTask before await (thread {Thread.CurrentThread.ManagedThreadId})");
await Task.Delay(10).ConfigureAwait(false);
Debug.Print($"InnerTask after await (thread {Thread.CurrentThread.ManagedThreadId}) - we are now on the thread pool");
}

This produces the following output:


Kicking off async chain (thread 6) - this is the main thread
OuterTask before await (thread 6)
InnerTask before await (thread 6)
InnerTask after await (thread 8) - we are now on the thread pool
OuterTask after await (thread 8)

Note that the code before the first await inside Task1 and even Task2 still executes on the "main" thread. Our chain actually executes synchronously, on the same thread which kicked off the outer task, until we await the first truly async operation (in this case Task.Delay).

Additionally

If you are running in an environment where SynchronizationContext.Current is not null (i.e. Windows Forms, WPF) and you're not using ConfigureAwait(false) on the tasks awaited inside your insert method, then continuations scheduled by the async state machine after the first await statement will also likely execute on the "main" thread - although this is not guaranteed in certain environments (i.e. ASP.NET).

Explanation, part 2 (executing Tasks on the thread pool)

If, as part of your insert method, you are opting to start any Tasks manually, then you are most likely scheduling your work on the thread pool by using Task.Run or any other method of starting a new task that does not specify TaskCreationOptions.LongRunning. Once the thread pool gets saturated any newly started tasks will be queued, thus reducing the throughput of your parallel system.

Proof:

IEnumerable<Task> tasks = Enumerable
.Range(0, 200)
.Select(_ => Task.Run(() => Thread.Sleep(100))); // Using Thread.Sleep to simulate blocking calls.

await Task.WhenAll(tasks); // Completes in 2+ seconds.

Now with TaskCreationOptions.LongRunning:

IEnumerable<Task> tasks = Enumerable
.Range(0, 200)
.Select(_ => Task.Factory.StartNew(
() => Thread.Sleep(100), TaskCreationOptions.LongRunning
));

await Task.WhenAll(tasks); // Completes in under 130 milliseconds.

It is generally not a good idea to spawn 200 threads (this will not scale well), but if massive parallelisation of blocking calls is an absolute requirement, the above snippet shows you one way to do it with TPL.

If async/await doesn't create new thread then explain this code

You're using a console application for your example. This effects greatly the outcome of your test. A console application has no custom SynchronizationContext (like Winforms, WPF and ASP.NET have), hence it uses the ThreadPoolTaskScheduler to schedule continuations on an arbitrary thread-pool thread. Try this same example in a UI application and you'll see the continuation invoked on the same thread.



Related Topics



Leave a reply



Submit