抛出异常时,使用Lazy <t>的StackOverflowException

时间:2017-11-18 14:16:25

标签: c#

一个非常简单的示例应用程序(.NET 4.6.2)在 12737 的递归深度处产生StackOverflowException,如果是 10243 ,则递减到递归深度 10243 大多数内部函数调用抛出一个异常,这是预期的和OK。

如果我使用Lazy<T>来短暂保存中间结果,则如果没有抛出异常且递归深度 2207 > 105 ,如果抛出异常。

注意:如果编译为x64,则深度为 105 的StackOverflowException只能被观察到。使用x86(32位)时,效果首先出现在 4272 的深度。单声道(就像在https://repl.it处使用的那样)在 74200 的深度下可以毫无问题地工作。

StackOverflowException不会在深度递归中发生,而是在升级回主程序时发生。最后一个块被深度处理,然后程序就死了:

Exception System.InvalidOperationException at 105
Finally at 105
...
Exception System.InvalidOperationException at 55
Finally at 55
Exception System.InvalidOperationException at 54
Finally at 54
Process is terminated due to StackOverflowException.

或在调试器中:

The program '[xxxxx] Test.vshost.exe' has exited with code -2147023895 (0x800703e9).

谁可以解释一下?

public class Program
{
    private class Test
    {
        private int maxDepth;

        private int CalculateWithLazy(int depth)
        {
            try
            {
                var lazy = new Lazy<int>(() => this.Calculate(depth));
                return lazy.Value;
            }  
            catch (Exception e)
            {
                Console.WriteLine("Exception " + e.GetType() + " at " + depth);
                throw;
            }
            finally
            {
                Console.WriteLine("Finally at " + depth);
            }
        }

        private int Calculate(int depth)
        {
            if (depth >= this.maxDepth) throw new InvalidOperationException("Max. recursion depth reached.");
            return this.CalculateWithLazy(depth + 1);
        }

        public void Run()
        {
            for (int i = 1; i < 100000; i++)
            {
                this.maxDepth = i;

                try
                {
                    Console.WriteLine("MaxDepth: " + i);
                    this.CalculateWithLazy(0);

                }
                catch { /* ignore */ }
            }
        }
    }

    public static void Main(string[] args)
    {
        var test = new Test();
        test.Run();
        Console.Read();
    }

更新:只需在递归方法中使用try-catch-throw块,就可以在不使用Lazy<T>的情况下重现问题。

        [MethodImpl(MethodImplOptions.NoInlining)]
        private int Calculate(int depth)
        {
            try
            {
                if (depth >= this.maxDepth) throw new InvalidOperationException("Max. recursion depth reached.");
                return this.Calculate2(depth + 1);
            }
            catch
            {
                throw;
            }
        }

        [MethodImpl(MethodImplOptions.NoInlining)]
        private int Calculate2(int depth) // just to prevent the compiler from tail-recursion-optimization
        {
            return this.Calculate(depth);
        }

        public void Run()
        {
            for (int i = 1; i < 100000; i++)
            {
                this.maxDepth = i;

                try
                {
                    Console.WriteLine("MaxDepth: " + i);
                    this.Calculate(0);

                }
                catch(Exception e)
                {
                    Console.WriteLine("Finished with " + e.GetType());
                }
            }
        }

2 个答案:

答案 0 :(得分:1)

有两个原因:

  1. Lazy在堆栈上使用更多内存(它只是一个变量)
  2. Lazy添加更多堆栈帧(因为它调用委托)
  3. 请阅读以下内容以了解更多详情:

    分配给调用堆栈的内存的数量是固定的(这是Thread maxStackSize

    因此,适合固定内存量的堆栈帧的数量取决于这些堆栈帧的大小

    如果在方法中使用其他变量,则必须将它们写入堆栈,并占用内存。

    此外,如果您使用Lazy<T>,堆栈帧的数量会有所不同,因为它包含一个需要再调用一次的委托(还有一个您不计算的堆栈帧)

    这正是您遇到的情况,如果您在lazy内使用额外的CalculateWithLazy变量,您的堆栈帧只占用更多空间,这就是为什么您在程序失败之前获得更少的堆栈帧的原因StackOverflowException

    可以更精确地计算出来,但我认为这种近似解释足以理解不同行为的原因。

    以下是如何找出线程的 maxStackSize 的方法: How to find current thread's max stack size in .net?

    以下是如何找出引用类型变量的大小(取决于平台+一些开销):How much memory does a C# reference consume?

    最后,您的代码中只有System.Int32,因此需要32个字节的内存 如果你有任何自定义结构(值类型)计算它们的大小将是一个相当大的挑战,请参阅@Hans Passant在这个问题中的答案: How do I check the number of bytes consumed by a structure?

答案 1 :(得分:0)

通过使用Lazy,您将添加更多调用:对Value属性的调用,可能是代理上的Invoke,可能更多取决于它是如何实现的。您的调试器调用堆栈可以帮助您查看正在进行的操作。