NuclearProgrammer
NuclearProgrammer

Reputation: 926

Infinite Recursive calls with Async/Await never throws exception

I was writing a small console app to try to become familiar with using async/await. In this app, I accidentally created an infinite recursive loop (which I have now fixed). The behavior of this infinitely recursive loop surprised me though. Rather than throwing a StackOverflowException, it became deadlocked.

Consider the following example. If Foo() is called with runAsync set to false, it throws a StackOverflowException. But when runAsync is true, it becomes deadlocked (or at least appears to). Can anyone explain why the behavior is so different?

bool runAsync;
void Foo()
{
    Task.WaitAll(Bar(),Bar());
}

async Task Bar()
{
   if (runAsync)
      await Task.Run(Foo).ConfigureAwait(false);
   else
      Foo();
}

Upvotes: 2

Views: 563

Answers (2)

i3arnon
i3arnon

Reputation: 116548

The async version doesn't deadlock (as usr explained) but it doesn't throw a StackOverflowException because it doesn't rely on the stack.

The stack is a memory area reserved for a thread (unlike the heap which is shared among all the threads).

When you call an async method it runs synchronously (i.e. using the same thread and stack) until it reaches an await on an uncompleted task. At that point the rest of the method is scheduled as a continuation and the thread is released (together with its stack).

So when you use Task.Run you are offloading Foo to another ThreadPool thread with a clean stack, so you'll never get a StackOverflowException.

You may however, reach an OutOfMemoryException because the async method's state-machine is stored in the heap, available for all threads to resume on. This example will throw very quickly because you don't exhaust the ThreadPool:

static void Main()
{
    Foo().Wait();
}

static async Task Foo()
{
    await Task.Yield();
    await Foo();
}

Upvotes: 1

usr
usr

Reputation: 171178

It's not really deadlocked. This quickly exhausts the available threads in the thread-pool. Then, one new thread is injected every 500ms. You can observe that when you put some Console.WriteLine logging in there.

Basically, this code is invalid because it overwhelms the thread-pool. Nothing in this spirit may be put into production.

If you make all waiting async instead of using Task.WaitAll you turn the apparent deadlock into a runaway memory leak instead. This might be an interesting experiment for you.

Upvotes: 6

Related Questions