C# Async/Await – Managing Different Threads with Asynchronous Programming

async-awaitasynchronousc++

I've heard a lot about how a new thread is not created with async await. I decided to check what would happen if there was an while(true) in the main function and the asynchronous function.

namespace asyncTest
{
    public class Program
    {

        public static void Task1()
        {
            while (true) {
                Console.WriteLine("Task1: " + Thread.CurrentThread.ManagedThreadId);
                Thread.Sleep(1000);
            }
        }

        public static async void async()
        {
            Console.WriteLine("async start: " + Thread.CurrentThread.ManagedThreadId);
            await Task.Run(Task1);
            Console.WriteLine("async end: " + Thread.CurrentThread.ManagedThreadId);
        }

        static void Main(string[] args)
        {
            async();
            while (true) {
                Console.WriteLine("main: " + Thread.CurrentThread.ManagedThreadId);
                Thread.Sleep(1000);
            }
        }
    }
}

But for some reason my functions are executed in different threads:

enter image description here

I want to understand why they are executed in different threads if async await does not create a thread. And I want to know how high-load operations are performed asynchronously, as in this example. Who performs the asynchronous operation on an additional thread? Or are they executed in the same thread?

Best Answer

I've heard a lot about how a new thread is not created with async await.

To quote the classic There Is No Thread by Stephen Cleary (highly recommend to read it, emphasis is mine):

This is an essential truth of async in its purest form: There is no thread.

The objectors to this truth are legion. “No,” they cry, “if I am awaiting an operation, there must be a thread that is doing the wait! It’s probably a thread pool thread. Or an OS thread! Or something with a device driver…”

Heed not those cries. If the async operation is pure, then there is no thread.

This does not mean that there are no threads involved at all though.

First of all you need to know that there are two "main" async scenarios: IO-bound and CPU-bound. From the Asynchronous programming scenarios:

The core of async programming is the Task and Task<T> objects, which model asynchronous operations. They are supported by the async and await keywords. The model is fairly simple in most cases:

  • For I/O-bound code, you await an operation that returns a Task or Task inside of an async method.
  • For CPU-bound code, you await an operation that is started on a background thread with the Task.Run method.

Your example basically simulates a CPU-bound scenario via endless cycle with Thread.Sleep in the Task1. It requires a thread to execute since Thread.Sleep is not an async operation:

Suspends the current thread for the specified amount of time.

Another case when thread is needed - to complete the continuation (i.e. what is written after await), but the thread might or might not be created here - in this case this is managed by the ThreadPool (see also the The managed thread pool doc):

Provides a pool of threads that can be used to execute tasks, post work items, process asynchronous I/O, wait on behalf of other threads, and process timers.

which manages the threads and will reuse them if there are available ones.

Note that in this case no extra thread will be created to perform the "waitng" on per await used basis.

As for IO-bound operations you can simulate an async operation with Task.Delay() for example. Consider the following modification to your code:

static async Task Main(string[] args)
{
    Task1(100);
    Task1(155);
    Task1(205);
    Task1(255);
    Task1(305);
    for (int i = 0; i < 10; i++)
    {
        Console.WriteLine("main: " + Thread.CurrentThread.ManagedThreadId);
        Thread.Sleep(500);
    }
}

public static async Task Task1(int i)
{
    while (true) {
        Console.WriteLine($"Task1 {i}: " + Thread.CurrentThread.ManagedThreadId);
        // Thread.Sleep(1000);
        await Task.Delay(1001 + i);
    }
}

depending on several factors you might see less unique threads in the output than you have simultaneously "running" async operations.

Notes:

  1. There are a lot o nuances here. For example:

    1. For IO-bound operation - it should be implemented correctly and that underlying system/device/driver supports it. For example Oracle driver for quite a long time didn't - see the Can the Oracle managed driver use async/await properly?

    2. Execution of continuation is more complicated too - it involves several factors, including presence of the SynchronizationContext. See also answers to What thread runs the code after the `await` keyword?

  2. async void

    Try avoiding this, in majority of cases it should be async Task (with exception of event handlers which usually are used in desktop/mobile apps)

  3. See also: