使用Async / Await的无限递归调用永远不会抛出异常

时间:2015-11-05 17:09:14

标签: c# .net recursion async-await task-parallel-library

我正在编写一个小型控制台应用程序,试图熟悉使用async / await。在这个应用程序中,我意外地创建了一个无限递归循环(我现在已经修复)。然而,这个无限递归循环的行为让我感到惊讶。它没有抛出StackOverflowException,而是陷入僵局。

考虑以下示例。如果在Foo()设置为runAsync的情况下调用false,则会引发StackOverflowException。但是当runAsynctrue时,它会陷入僵局(或至少看起来像)。谁能解释为什么行为如此不同?

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

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

2 个答案:

答案 0 :(得分:4)

它并没有真正陷入僵局。这很快耗尽了线程池中的可用线程。然后,每500ms注入一个新线程。当你在那里放一些Console.WriteLine日志时,你可以观察到。

基本上,这段代码无效,因为它压倒了线程池。这种精神没有任何东西可以投入生产。

如果您使所有等待异步而不是使用Task.WaitAll,则将明显的死锁转换为失控的内存泄漏。这可能是一个有趣的实验。

答案 1 :(得分:1)

异步版本没有死锁(正如我们解释的那样),但它并没有抛出StackOverflowException因为它不依赖于堆栈。

堆栈是为线程保留的内存区域(与在所有线程之间共享的堆不同)。

当你调用异步方法时,它会同步运行(即使用相同的线程和堆栈),直到它等待未完成的任务。此时,方法的其余部分被安排为继续,并且线程被释放(与其堆栈一起)。

因此,当您使用Task.Run时,您正在使用干净的堆栈将Foo卸载到另一个ThreadPool线程,因此您永远不会得到StackOverflowException

但是,您可以达到OutOfMemoryException,因为异步方法的状态机存储在堆中,可供所有线程继续使用。这个例子很快就会抛出,因为你不会耗尽ThreadPool

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

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