Loreno
Loreno

Reputation: 668

Why does my code run on multiple threads?

Since a pretty long time I'm trying to understand async-await stuff in .NET, but I struggle to succeed, there's always something totally unexpected happening when I use async.

Here's my application:

namespace ConsoleApp3
{
    class Program
    {
        static async Task Main(string[] args)
        {
            Console.WriteLine("Hello World!");

            var work1 = new WorkClass();
            var work2 = new WorkClass();

            while(true)
            {
                work1.DoWork(500);
                work2.DoWork(1500);
            }
        }
    }

    public class WorkClass
    {
        public async Task DoWork(int delayMs)
        {
            var x = 1;

            await Task.Delay(delayMs)

            var y = 2;
        }
    }
}

It's just a sample that I created to check how the code will be executed. There are a few things that surprise me. First off, there are many threads involved! If I set a breakpoint on var y = 2; I can see that threadId is not the same there, it can be 1, or 5, or 6, or something else. Why is that? I thought that async/await does not use additional threads on its own unless I explicitly command that (by using Task.Run or creating a new Thread). At least this article tries to say that I think.

Ok, but let's say that there are some other threads for whatever reason - even if they are, my await Task.Delay(msDelay); does not have ConfigureAwait(false)! As I understand it, without this call, thread shouldn't change.

It's really difficult for me to grasp the concept well, because I cannot find any good resource that would contain all information instead of just a few pieces of information.

Upvotes: 0

Views: 211

Answers (2)

Theodor Zoulias
Theodor Zoulias

Reputation: 43643

Async/await does not use additional threads on its own, but in your example it is not on its own. You are calling Task.Delay, and this method schedules a continuation to run in a thread-pool thread. There is no thread blocked during the delay though. A new thread is not created. When the time comes an existing thread is used to run the continuation, which in your case has very little work to do (just run the var y = 2 assignment), because you are not even awaiting the task returned by DoWork. When this work is done (a fraction of a microsecond later) the thread-pool thread is free again to do other jobs.

Instead of Task.Delay you could await another method that makes no use of threads at all, or a method that creates a dedicated long running thread, or a method that starts a new process. Async/await is not responsible for any of these. Async/await is just a mechanism for creating task continuations in a developer-friendly way.

Here is your application modified for a world without async/await:

class Program
{
    static Task Main(string[] args)
    {
        Console.WriteLine("Hello World!");

        var work1 = new WorkClass();
        var work2 = new WorkClass();

        while (true)
        {
            work1.DoWork(500);
            work2.DoWork(1500);
        }
    }
}

public class WorkClass
{
    public Task DoWork(int delayMs)
    {
        var x = 1;

        int y;

        return Task.Delay(delayMs).ContinueWith(_ =>
        {
            y = 2;
        });
    }
}

Upvotes: 1

Jon Skeet
Jon Skeet

Reputation: 1501626

When an asynchronous method awaits something, if it's not complete, it schedules a continuation and then returns. The question is which thread the continuation runs on. If there's a synchronization context, the continuation is scheduled to run within that context - typically a UI thread, or potentially a specific pool of threads.

In your case, you're running a console app which means there is no synchronization context (SynchronizationContext.Current will return null). In that case, continuations are run on thread pool threads. It's not that a new thread is specifically created to run the continuation - it's just that the thread pool will pick up the continuation, whereas the "main" thread won't run the continuation.

ConfigureAwait(false) is used to indicate that you don't want to return to the current synchronization context for the continuation - but as there's no synchronization context anyway in your case, it would make no difference.

Upvotes: 8

Related Questions