33

我对await关键字的工作原理有一个脆弱的掌握,我想扩展我对它的理解。

仍然让我头晕目眩的问题是递归的使用。这是一个例子:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace TestingAwaitOverflow
{
    class Program
    {
        static void Main(string[] args)
        {
            var task = TestAsync(0);
            System.Threading.Thread.Sleep(100000);
        }

        static async Task TestAsync(int count)
        {
            Console.WriteLine(count);
            await TestAsync(count + 1);
        }
    }
}

这显然抛出了一个StackOverflowException.

我的理解是因为代码实际上是同步运行的,直到第一个异步操作,之后它返回一个Task包含异步操作信息的对象。在这种情况下,没有异步操作,因此它只是在它最终会得到一个Task返回的错误承诺下不断递归。

现在稍微改变一下:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace TestingAwaitOverflow
{
    class Program
    {
        static void Main(string[] args)
        {
            var task = TestAsync(0);
            System.Threading.Thread.Sleep(100000);
        }

        static async Task TestAsync(int count)
        {
            await Task.Run(() => Console.WriteLine(count));
            await TestAsync(count + 1);
        }
    }
}

这个不扔StackOverflowException。我可以理解它为什么起作用,但我更倾向于将其称为直觉(它可能涉及如何安排代码使用回调来避免构建堆栈,但我无法将这种直觉转化为解释)

所以我有两个问题:

  • 第二批代码如何避免a StackOverflowException
  • 第二批代码会不会浪费其他资源?(例如,它是否在堆上分配了大量的 Task 对象?)

谢谢!

4

2 回答 2

17

任何函数中直到第一个等待的部分都是同步运行的。在第一种情况下,它会因此而陷入堆栈溢出 - 没有任何东西会中断调用自身的函数。

第一个 await (它不会立即完成 - 这很可能是您的情况)导致函数返回(并放弃其堆栈空间!)。它将其余部分排队作为延续。TPL 确保延续永远不会嵌套太深。如果存在堆栈溢出的风险,则继续排队到线程池,重置堆栈(开始填满)。

第二个例子仍然可以溢出!如果Task.Run任务总是立即完成怎么办?(这不太可能,但可以通过正确的操作系统线程调度来实现)。然后,异步函数将永远不会被中断(导致它返回并释放所有堆栈空间),并且会产生与情况 1 相同的行为。

于 2012-12-10T19:57:37.497 回答
0

在您的第一个和第二个示例中, TestAsync 仍在等待对自身的调用返回。不同之处在于递归是在第二种方法中打印并将线程返回到其他工作。因此递归不够快,不足以成为堆栈溢出。但是,第一个任务仍在等待,最终计数将达到其最大整数大小,否则将再次引发堆栈溢出。关键是调用线程被返回,但实际的异步方法被安排在同一个线程上。基本上,TestAsync 方法在等待完成之前会被遗忘,但它仍保留在内存中。允许线程做其他事情,直到等待完成,然后该线程被记住并在等待停止的地方完成。额外的等待调用存储线程并再次忘记它,直到等待再次完成。直到所有等待都完成并且方法因此完成,TaskAsync 仍在内存中。所以,事情就是这样。如果我告诉一个方法做某事,然后调用等待任务。我在其他地方的其余代码继续运行。当 await 完成时,代码会重新回到那里并完成,然后回到它之前的那个时间正在做的事情。在您的示例中,您的 TaskAsync 始终处于墓碑状态(可以这么说),直到最后一次调用完成并将调用返回到链中。当 await 完成时,代码会重新回到那里并完成,然后回到它之前的那个时间正在做的事情。在您的示例中,您的 TaskAsync 始终处于墓碑状态(可以这么说),直到最后一次调用完成并将调用返回到链中。当 await 完成时,代码会重新回到那里并完成,然后回到它之前的那个时间正在做的事情。在您的示例中,您的 TaskAsync 始终处于墓碑状态(可以这么说),直到最后一次调用完成并将调用返回到链中。

编辑:我一直说存储线程或那个线程,我的意思是例程。它们都在同一个线程上,这是您示例中的主线程。对不起,如果我让你感到困惑。

于 2014-08-07T05:12:20.490 回答