Async/await, There is no thread – it is easy to fool those who want to fool themselves

Async/await, There is no thread – it is easy to fool those who want to fool themselves

When we are shown on some example that an asynchronous operation does not create a thread, we are tried to convince that an asynchronous operation NEVER creates a thread and in principle cannot create one, but this is not true! A simple example with working code proves otherwise. Let’s analyze this example.

The logic of those who succumb to such suggestion is completely clear to me, they want to simplify their lives, reduce the amount of theory that needs to be dealt with.

It would be interesting to understand the logic of those who support such a suggestion, passing off a truncated theory as a full-fledged one, fully aware that everything is not as simple as one would like.


So, an example code that will create an additional thread for an asynchronous opcode in a console (this is important! why, we’ll see later) application looks like this:

        static async Task Main()
        {
            SomeMethodAsync1();
            Console.WriteLine($"ThrID={Thread.CurrentThread.ManagedThreadId}");
            for (int i = 0; i < 5; i++)
            {
                Console.WriteLine($"MAIN Iter={i}");
                Thread.Sleep(100);
            }
        }

        static async Task SomeMethodAsync1()
        {
            await Task.Yield();
            Console.WriteLine($"TrID={Thread.CurrentThread.ManagedThreadId}");
            for (int i = 0; i < 5; i++)
            {
                Console.WriteLine($"MethodIter={i}");
                Thread.Sleep(100);
            }
        }

This code generates the following output:

Actually, from this conclusion it is clear that the two functions are performed in different streams with identifiers: ID=1 and ID=5.

You can take a closer look at these threads N1 and N5 in debug mode:

Not Flagged                12268  1          Main Thread   Main Thread            System.Private.CoreLib.dll!System.Threading.Thread.Sleep

Not Flagged    >           26956  5          Worker Thread           .NET ThreadPool Worker            PrimMath.dll!PrimMath.Program.SomeMethodAsync1

We see that our streams are taken from the Trade pool, that is, formally speaking, our function SomeMethodAsync1() does not create a thread, it takes an existing thread from the thread pool. But it cannot be denied that an asynchronous operation still uses an additional thread, that is, an assertion There is no thread turns out to be false for this code, because we can clearly see that thread with ID5 is definitely present.

But maybe I’m somehow deceiving you too? If you don’t believe me and my example and your eyes, you can also refer to the unforgettable Stephen Toub and his already tired of everyone (I hope it’s not 🙂 work How Async/Await Really Works in C#. Paragraph:

SynchronizationContext and ConfigureAwait

There is an example of console code that is referred to later in that post as our timing sample and which also creates an additional stream for async lambda functions. The fact that the async method creates an extra thread is a problem that the author of the Post later successfully solves when he replaces the SynchronizationContext for this original example. Actually, from that example of Steven Tobe, you should have concluded that the presence or absence of a flow inside an asynchronous method is controlled by the SynchronizationContext, which, in a sense, can be considered an additional scheduler that is provided to us by the .NET runtime. and which you can redefine for your tasks at any moment.

And now armed with this knowledge about the presence or absence of flow
for asynchronous operations, manages the synchronization context (SynchronizationContext),
let’s look at the example discussed in the article There is no thread. (There is no stream):

private async void Button_Click(object sender, RoutedEventArgs e)
{
  byte[] data = ...
  await myDevice.WriteAsync(data, 0, data.Length);
}

I want to draw your attention to the name of the function Button_Click(), which unambiguously tells us which SynchronizationContext is used in this example, is the SynchronizationContext of the current window (or the entire UI). The UI SynchronizationContext doesn’t really create threads, it wraps asynchronous calls into messages for a single UI thread, queues them up for execution on that single thread.

That’s why I pointed out at the very beginning that my example code (as well as Tobe’s example code from the SynchronizationContext paragraph) runs in a console application, the windowed application and the console application use a different source SynchronizationContext, which means they are different control the streaming model that is used in asynchronous methods.

Well, I hope I answered the question from @Grave18

Can the async/await magic create threads by itself, or is it all done explicitly by the person writing the async functions?

Thanks again for the constructive question.

Related posts