递归和等待/异步关键字

时间:2012-12-10 19:54:22

标签: c# recursion async-await

我对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。我可以 sortof 看看为什么它有效,但我会更多地称之为直觉(它可能涉及代码如何安排使用回调以避免构建堆栈,但我无法翻译那种直觉感觉到了解释)

所以我有两个问题:

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

谢谢!

2 个答案:

答案 0 :(得分:17)

任何函数中第一个await的部分同步运行。在第一种情况下,由于这种情况,它会进入堆栈溢出 - 没有任何事情会中断调用自身的函数。

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

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

答案 1 :(得分:0)

在您的第一个和第二个示例中,TestAsync仍在等待对其自身的调用返回。区别在于递归是打印并将线程返回到第二种方法中的其他工作。因此递归不够快,不能成为堆栈溢出。但是,第一个任务仍在等待,最终计数将达到它的最大整数大小或将再次抛出堆栈溢出。重点是返回调用线程,但实际的异步方法是在同一个线程上调度的。基本上,在等待完成之前忘记了TestAsync方法,但它仍然保存在内存中。允许该线程执行其他操作,直到等待完成,然后该线程被记住并完成等待中断的位置。额外的等待调用存储线程并再次忘记它,直到等待再次完成。直到所有等待完成并且该方法因此完成TaskAsync仍然在内存中。所以,这就是事情。如果我告诉方法做某事然后调用等待任务。我在其他地方的其他代码继续运行。等待完成后,代码会在那里重新启动并完成,然后回到它之前的那个时间。在您的示例中,您的TaskAsync始终处于逻辑删除状态(可以这么说),直到最后一次调用完成并将调用返回到链中。

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