Async thread body loop, It just works, but how?
Asked Answered
N

1

8

I have just tested something that I was sure would fail miserably, but to my surprise, it worked flawlessly, and proves to myself that I am still quite mystified by how async-await works.

I created a thread, passing an async void delegate as the thread's body. Here's an oversimplification of my code:

var thread = new Thread( async () => {
   while( true ) {
      await SomeLengthyTask();
      ...
   }
});
thread.Start();
thread.Join();

The thing is that as far as I understand, when the execution hits the await keyword, there is an implicit return from the method, in this case the body of the looping thread, while the rest of the code is wrapped in a callback continuation.

Because of this fact, I was pretty sure that the thread would terminate as soon as the await yielded execution, but that's not the case!

Does anybody know how this magic is actually implemented? Is the async functionality stripped down and the async waits synchronously or is there some black magic being done by the CLR that enables it to resume a thread that has yielded because of an await?

Nutritious answered 5/5, 2015 at 5:22 Comment(0)
W
8

The thread is indeed terminated very quickly.

But since the Thread constructor doesn't accept an async lambda what you got there is an async void delegate.

The original thread will end and the continuation (the rest after the await) will be posted to the ThreadPool and eventually run on another thread.

You can test that by checking the thread id:

var thread = new Thread(async () =>
{
    while (true)
    {
        Console.WriteLine(Thread.CurrentThread.ManagedThreadId);
        await SomeLengthyTask();
        Console.WriteLine(Thread.CurrentThread.ManagedThreadId);
    }
});
thread.Start();
thread.Join();

Console.ReadLine();

Output:

3
5
5
...
5

To make the example simpler let's assume you have this Run method:

void Run(Action action)
{
    action();
}

And you call it with your async delegate

Run(async () => 
{
    while(true) 
    {
      await SomeLengthyTask();
      ...
    }
});

The execution of Run will complete almost immediately when it reaches the first await and returns. The rest of the async delegate will continue on the ThreadPool with another thread.


Generally, each time you reach an await in the execution of an async method the thread is lost and the continuation (the rest after the awaited task completes) will be posted to the ThreadPool (unless if there's a SynchronizationContext present, like in the UI thread). It may be that it execution will be on the same thread (as in my example with 5) but it also may not.

In your case the thread you create explicitly isn't part of the ThreadPool so it will definitely be terminated and the rest will run on a different thread.

Wake answered 5/5, 2015 at 5:33 Comment(7)
Thanks for the correct clarification between lambda and delegate, I edited my question accordingly. So, to understand, what the CLR is actually doing is simply preventing the Join() of the original thread to take place?Nutritious
@Nutritious No, it does takes place. That's actually why I added a Console.ReadLine in my example. Without it my console application ends before reaching the interesting part.Wake
@Nutritious There's nothing special being done to Thread here. async methods just don't run on the same thread. They may do that, but as long as there isn't a SynchronizationContext they run each synchronous part on the ThreadPoolWake
Great answer, thanks for the insight - I guess that it is a very expensive way of maintaining a thread loop ;)Nutritious
@Nutritious If you use Task.Run instead of creating your own thread it can actually be very scalable. It depends on how async are the contents of your while. If you do something and wait for a minute and do it again etc. it's much better than waiting synchronously. https://mcmap.net/q/245320/-best-way-to-do-a-task-looping-in-windows-serviceWake
@Nutritious Actually, it's a pretty common pattern that works rather well. The main question is whether it makes sense to await in the first place - if you're doing some I/O or Task.Delay or something like that, it should dwarf the overhead of scheduling the thread pool work items. But it's completely useless to create the thread - just use Task.Run. It's used for stuff like main connection accept loops in network servers - async void method that has while (true) { var s = await listener.AcceptAsync(); HandleAcceptAsync(s); } (HandleAcceptAsync also being async void method).Abstractionist
@Nutritious If you want to keep track of the top level (while (true)) task, just have it be an async Task method - don't await on that task, just keep it around. Then you can use e.g. Task.IsCompleted to see if it's still running, or something like Task.Wait to prevent the application from exiting while the loop is still running (a pretty common pattern in console applications) - be careful with that, though; I'd only use it for the top-level while (true) loop.Abstractionist

© 2022 - 2024 — McMap. All rights reserved.