在递归方法中使用async / await的正确方法是什么?

时间:2014-07-29 03:14:42

标签: c# .net asynchronous recursion async-await

在递归方法中使用async / await的正确方法是什么?这是我的方法:

public string ProcessStream(string streamPosition)
{
    var stream = GetStream(streamPosition);

    if (stream.Items.count == 0)
        return stream.NextPosition;

    foreach(var item in stream.Items) {
        ProcessItem(item);
    }

    return ProcessStream(stream.NextPosition)
}

以下是使用async / await的方法:

public async Task<string> ProcessStream(stringstreamPosition)
{
        var stream = GetStream(streamPosition);

        if (stream.Items.count == 0)
            return stream.NextPosition;

        foreach(var item in stream.Items) {
            await ProcessItem(item); //ProcessItem() is now an async method
        }

        return await ProcessStream(stream.NextPosition);
 }

2 个答案:

答案 0 :(得分:6)

虽然我必须提前说明方法的意图并不完全清楚,但用一个简单的循环重新实现它是非常简单的:

public async Task<string> ProcessStream(string streamPosition)
{
    while (true)
    {
        var stream = GetStream(streamPosition);

        if (stream.Items.Count == 0)
            return stream.NextPosition;

        foreach (var item in stream.Items)
        {
            await ProcessItem(item); //ProcessItem() is now an async method
        }

        streamPosition = stream.NextPosition;
    }
}

递归不是堆栈友好的,如果您可以选择使用循环,那么在简单的同步场景中(值得控制的递归最终导致StackOverflowException s),这是值得研究的东西,以及异步场景,其中,老实说,我甚至不知道如果你把事情推得太远会发生什么(每当我尝试使用{重现已知的堆栈溢出场景时,我的VS测试资源管理器崩溃了{1}}方法)。

Recursion and the await / async Keywords等答案表明由于async状态机的工作方式,StackOverflowExceptionasync的问题较少,但这不是我的问题尽可能地探索,因为我倾向于避免递归。

答案 1 :(得分:1)

当我添加代码以使您的示例更具体时,我发现两种可能的方式使递归变得非常糟糕。他们都认为你的数据很大,需要特定的条件来触发。

  1. 如果ProcessItem(string)返回在Task之前完成的await(或者,我认为它在await完成旋转之前完成),则继续将同步执行。在下面的代码中,我通过让ProcessItem(string)返回Task.CompletedTask来模拟这一点。当我这样做时,程序 very 很快就会终止StackOverflowException。这是因为.net的TPL“Releases Zalgo”由opportunistically executing continuations synchronously而不考虑当前堆栈中有多少可用空间。这意味着它将通过使用递归算法加剧您已经拥有的潜在堆栈空间问题。要查看此内容,请在下面的代码示例中注释掉await Task.Yield();
  2. 如果您使用某种技术来防止TPL异步连续(我使用Task.Yield()下面),最终程序将耗尽内存并死于OutOfMemoryException。如果我理解正确,如果return await能够模拟尾部调用优化,则不会发生这种情况。我想这里发生的事情是每次调用都会生成类似于簿记Task<string>的内容,并且即使它们可以合并,也会继续生成它们。要使用下面的示例重现此错误,请确保您将程序作为32位运行,禁用Console.WriteLine()调用(因为控制台非常慢),并确保取消注释await Task.Yield()
  3. using System;
    using System.Collections.Generic;
    using System.Threading.Tasks;
    
    // Be sure to run this 32-bit to avoid making your system unstable.
    class StreamProcessor
    {
        Stream GetStream(string streamPosition)
        {
            var parsedStreamPosition = Convert.ToInt32(streamPosition);
            return new Stream(
                // Terminate after we reach 0.
                parsedStreamPosition > 0 ? new[] { streamPosition, } : new string[] { },
                Convert.ToString(parsedStreamPosition - 1));
        }
    
        Task ProcessItem(string item)
        {
            // Comment out this next line to make things go faster.
            Console.WriteLine(item);
            // Simulate the Task represented by ProcessItem finishing in
            // time to make the await continue synchronously.
            return Task.CompletedTask;
        }
    
        public async Task<string> ProcessStream(string streamPosition)
        {
            var stream = GetStream(streamPosition);
    
            if (stream.Items.Count == 0)
                return stream.NextPosition;
    
            foreach (var item in stream.Items)
            {
                await ProcessItem(item); //ProcessItem() is now an async method
            }
    
            // Without this yield (which prevents inline synchronous
            // continuations which quickly eat up the stack),
            // you get a StackOverflowException fairly quickly.
            // With it, you get an OutOfMemoryException eventually—I bet
            // that “return await” isn’t able to tail-call properly at the Task
            // level or that TPL is incapable of collapsing a chain of Tasks
            // which are all set to resolve to the value that other tasks
            // resolve to?
            await Task.Yield();
    
            return await ProcessStream(stream.NextPosition);
        }
    }
    
    class Program
    {
        static int Main(string[] args) => new Program().Run(args).Result;
        async Task<int> Run(string[] args)
        {
            await new StreamProcessor().ProcessStream(
                Convert.ToString(int.MaxValue));
            return 0;
        }
    }
    
    class Stream
    {
        public IList<string> Items { get; }
        public string NextPosition { get; }
        public Stream(
            IList<string> items,
            string nextPosition)
        {
            Items = items;
            NextPosition = nextPosition;
        }
    }
    

    所以,我想我的两条建议是:

    1. 如果您不确定递归的堆栈增长会被其他内容中断,请使用Task.Yield()
    2. As suggested already,如果首先对你的问题没有意义,请避免递归。即使它是一个干净的算法,如果您的问题规模无限制,请避免使用它。