简体   繁体   English

递归和等待/异步关键字

[英]Recursion and the await / async Keywords

I have a fragile grasp of how the await keyword works, and I want to extend my understanding of it a bit. 我对await关键字的工作方式有一个脆弱的把握,我想稍微扩展一下我对它的理解。

The issue that still makes my head spin is the use of recursion. 仍然让我头疼的问题是使用递归。 Here's an example: 这是一个例子:

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);
        }
    }
}

This one obviously throws a StackOverflowException . 这个显然抛出了StackOverflowException

My understanding is because the code actually runs synchronously until the first asynchronous action, after which it returns a Task object that contains information about the asynchronous operation. 我的理解是因为代码实际上是同步运行的,直到第一个异步操作,之后它返回一个包含异步操作信息的Task对象。 In this case, there is no asynchronous operation, thus it just keeps recursing under the false promise that it will eventually get a Task returned. 在这种情况下,没有异步操作,因此它只是在错误的承诺下继续递归,它最终会返回一个Task

Now changing it just a tiny bit: 现在改变它只是一点点:

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);
        }
    }
}

This one does not throw a StackOverflowException . 这个不会抛出StackOverflowException I can sortof see why it works, but I would call it more of a gut feeling (it probably deals with how the code is arranged to use callbacks to avoid building the stack, but I can't translate that gut feeling into an explanation) 我可以理解为什么它有效,但我会更多地称之为直觉(它可能涉及代码如何安排使用回调以避免构建堆栈,但我无法将这种直觉转化为解释)

So I have two questions: 所以我有两个问题:

  • How does the second batch of code avoid a StackOverflowException ? 第二批代码如何避免StackOverflowException
  • Does the second batch of code waste other resources? 第二批代码是否浪费了其他资源? (for example does it allocate an absurdly large number of Task objects on the heap?) (例如,它是否在堆上分配了大量的荒谬的Task对象?)

Thanks! 谢谢!

The part up to the first await in any function runs synchronously. 任何函数中第一个等待的部分同步运行。 In the first case it runs into a stack overflow because of that - there is nothing interrupting the function calling itself. 在第一种情况下,由于这种情况,它会遇到堆栈溢出 - 没有任何事情会中断调用自身的函数。

The first await (which does not complete immediately - this is the case for you with high likelyhood) causes the function to return (and to give up its stack space!). 第一个await(它没有立即完成 - 对于你有很高可能性的情况)导致函数返回(并放弃它的堆栈空间!)。 It queues the rest of it as a continuation. 它将其余部分排成一个延续。 The TPL ensures that continuations never nest too deeply. TPL确保延续从不嵌套太深。 If there is a risk of stack overflow the continuation is queued to the thread pool, resetting the stack (which was starting to fill up). 如果存在堆栈溢出的风险,则继续将排队到线程池,重置堆栈(它开始填满)。

The second example can still overflow! 第二个例子仍然可以溢出! What if the Task.Run task always completed immediately? 如果Task.Run任务总是立即完成怎么办? (This is unlikely but possible with the right OS thread scheduling). (这不太可能,但可以使用正确的OS线程调度)。 Then, the async function would never be interrupted (causing it to return and free all stack space) and the same behavior as in case 1 would result. 然后,async函数永远不会被中断(导致它返回并释放所有堆栈空间),并且会产生与情况1相同的行为。

In your first and second example the TestAsync is still waiting for the call to itself to return. 在您的第一个和第二个示例中,TestAsync仍在等待对自身的调用返回。 The difference is the recursion is printing and returning the thread to other work in the second method. 区别在于递归是打印并将线程返回到第二种方法中的其他工作。 Therefore the recursion isn't fast enough to be a stack overflow. 因此,递归速度不足以成为堆栈溢出。 However, the first task is still waiting and eventually count will reach it's max integer size or stack overflow will be thrown again. 但是,第一个任务仍在等待,最终计数将达到它的最大整数大小,否则将再次抛出堆栈溢出。 The point is the calling thread is returned to but the actual async method is scheduled on the same thread. 重点是返回调用线程,但实际的异步方法是在同一个线程上调度的。 Basically, the TestAsync method is forgotten until await is complete but it is still held in memory. 基本上,在等待完成之前忘记了TestAsync方法,但它仍然保存在内存中。 The thread is allowed to do other things until await completes and then that thread is remembered and finishes where await left off. 允许该线程执行其他操作,直到等待完成,然后该线程被记住并完成等待中断的位置。 Additional await calls store the thread and forget it again until await is again complete. 额外的等待调用存储线程并再次忘记它,直到等待再次完成。 Until all awaits are completed and the method therefore completes the TaskAsync is still in memory. 直到所有等待完成并且该方法因此完成TaskAsync仍然在内存中。 So, here's the thing. 所以,这就是事情。 If I tell a method to do something and then call await for a task. 如果我告诉方法做某事然后调用等待任务。 The rest of my codes elsewhere continues running. 我在其他地方的其他代码继续运行。 When await is complete the code picks back up there and finishes and then goes back to what it was doing at that time right before it. 等待完成后,代码会在那里重新启动并完成,然后回到它之前的那个时间。 In your examples your TaskAsync is always in a tombstoned state (so to speak) until the last call complete and returns the calls back up the chain. 在您的示例中,您的TaskAsync始终处于逻辑删除状态(可以这么说),直到最后一次调用完成并将调用返回到链中。

EDIT: I kept saying store the thread or that thread and I meant routine. 编辑:我一直说存储线程或那个线程,我的意思是例程。 They are all on the same thread which is the main thread in your example. 它们都在同一个线程中,这是您示例中的主线程。 Sorry if I confused you. 对不起,如果我困惑你。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM