递归和等待/异步关键字

riw*_*alk 29 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);
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

这显然是一个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);
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

这一个不扔StackOverflowException.我可以理解为什么它有效,但我会更多地称之为直觉(它可能涉及代码如何安排使用回调以避免构建堆栈,但我无法将这种直觉转化为解释)

所以我有两个问题:

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

谢谢!

usr*_*usr 17

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

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

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

  • @ Stargazer712是的!如果你用`Task.Yield()`(它保证需要发布延续)替换它,你将获得保证不受堆栈溢出的影响(以性能成本).注意,`Yield`返回除了`Task`之外的等待.从任务中获得此保证要困难得多,因为在await功能查询时必须确保它没有完成. (5认同)