有没有办法在递归调用之前检查可用的堆栈大小? (C#)

Posted

技术标签:

【中文标题】有没有办法在递归调用之前检查可用的堆栈大小? (C#)【英文标题】:Is there a way to check available stack size before recursive call? (C#) 【发布时间】:2012-09-02 15:54:14 【问题描述】:

对于 C# AI 程序,我使用递归调用来寻找最佳下一步行动(使用 30x30 数组来存储当前棋盘状态)。对于我所做的每一步,我想看看我可以从新的棋盘状态中做出的哪些可能的移动是最好的......等等,直到我到达“游戏结束”的位置(在那个位置上没有进一步的移动可能)状态)或计时器停止该过程并且不再进行递归调用(并且返回“最佳”已知位置)。这只是为了解释为什么我必须使用递归(它不是尾递归)并且我不能使用单个(全局)棋盘状态,而是必须从当前状态中搜索所有可能的棋盘状态。

(有时)我得到一个 System.***Exception。有没有办法在下一次递归调用之前检查可用的堆栈空间?然后我可以将当​​前状态作为“迄今为止找到的最佳位置”返回,而不是进行下一次递归调用。 IE。当可用堆栈变得太小时,它也应该算作基本情况。

当然,另一种选择可能是将每个递归调用放在 try..catch 块中,并通过使用它作为基本情况来处理 System.***Exception?

【问题讨论】:

重新设计你的代码? *** 是错误或错误 (C#) 代码的标志。您需要大量的递归调用来触发 ***。如果您真的想这样做,请使用支持尾调用的函数式语言,例如 F#。 C# 不是为它设计的。 "如果您正在调用递归方法或计划使用大量堆栈空间,则必须使用 RuntimeHelpers.ExecuteCodeWithGuaranteedCleanup 方法。" -- msdn.microsoft.com/en-us/library/… 【参考方案1】:

实际上,如果现有堆栈空间不足,系统会动态扩展堆栈大小。因此,即使您可以测试堆栈的大小,也无所谓。

http://msdn.microsoft.com/en-us/library/windows/desktop/ms686774(v=vs.85).aspx详情

系统根据需要从保留的堆栈内存中提交额外的页面,直到堆栈达到保留大小减去一页(用作防止堆栈溢出的保护页面)或系统内存不足操作失败”。

也就是说,在递归发生之前,堆栈是一个大小;如果递归导致堆栈溢出,则堆栈在发生这种情况时是一个新的大小。

由于您无法捕获***Exception,因此您可以使用尾递归来代替终端递归。以下链接提供了一些关于将终端recusion 转换为tail recusion 的详细信息:http://www.thomaslevesque.com/2011/09/02/tail-recursion-in-c/

【讨论】:

“(有时)我得到一个 System.***Exception。” (可能有助于解释这一点。) @DavidO,这并不是说尽管在递归进行时增加了堆栈,但它耗尽了内存来扩展堆栈。 取决于当前棋盘位置。如果足够早地找到答案(基本案例及早发现),我通常不会遇到问题。【参考方案2】:

您可以使用队列 + 循环 (Queue<TNode> + while (queue.MoveNext())) 代替递归并限制队列的大小。

或者您可以将 open 调用 计数到该方法并以这种方式限制递归。 (计数条目和退出,如果条目 - 存在 > maxOpenCalls,则不进入递归)。

【讨论】:

【参考方案3】:

从 .NET 2 开始,您无法捕获 ***Exception...

确定您的堆栈已经使用了多少的唯一方法是使用我强烈建议不要使用的不安全代码...最好使用基于堆的显式Stack<T>

【讨论】:

【参考方案4】:

如果你真的想走这条路,你可以使用EnsureSufficientExecutionstack 方法。

正如其他人指出的那样,从 .NET 2.0 开始,您无法捕获***Exception,但是,从 MSDN 文档中您知道之前的方法具有以下行为:

确保剩余的堆栈空间足够大,可以执行 .NET Framework 的平均功能。

当根据这种方法堆栈不够大时,它会抛出一个你可以捕获InsufficientExecutionStackException异常。

【讨论】:

你试过了吗? “确保剩余的堆栈空间足够大,可以执行一般的 .NET Framework 函数”。在 OP 的情况下永远不会有足够的空间——这个方法怎么会知道呢? 每个递归级别都必须重新检查,不是吗? 那部分来自 MSDN 文档,我应该为它添加一个引用,我会更新答案。正如 DavidO 指出的那样,需要在每个步骤中执行检查才能成为有效的解决方法。 @DavidO 嗯,我想可以。但是在每次调用递归方法时调用RuntimeHelpers.EnsureSufficientExecutionStack?哎呀,我只能想象你会受到的性能打击...... 如果性能是一个问题,递归可能不是首选工具。无论如何,如果代码无法重构为迭代方法,或者无法在早期阶段控制递归,我认为这就是答案。【参考方案5】:

其实你可以捕捉到 *** 的执行,当然递归方法必须做一些配合 你做一个这样的方法:

void Zoo()
    
        RuntimeHelpers.EnsureSufficientExecutionStack();
        int[] baba = new int[1024 * 5];
        Zoo();
    

那就这样称呼吧

 try
        
            Zoo();
        
        //catch (Exception ex)
        catch(InsufficientExecutionStackException ex)
        
            ex.ProcessException().Show("Good God what are you doing");
        

这就是进程异常方法的工作原理

public static class Helper

[System.Runtime.InteropServices.DllImport("kernel32.dll")]
    public static extern uint GetCurrentThreadId();

public static string ProcessException(this Exception ex)
    
        StringBuilder strBuild = new StringBuilder(5000);
        if (ex is InsufficientExecutionStackException)
        
            strBuild.AppendLine("#%#%#%#%#% We Ran out of Stack Space on thread id : " + GetCurrentThreadId().ToString() + " @ :" + DateTime.Now.ToString() + " #%#%#%#%#%");
            strBuild.AppendLine(ex.Message);
            string[] ribals = ex.StackTrace.Split('\n');
            strBuild.AppendLine(String.Join("\n", ribals.Take(3).ToArray()));
            strBuild.AppendLine("\nLike this you can have many more lines ...\n");
            strBuild.AppendLine("Main issue  found here :\n" + ribals.Last());
            strBuild.AppendLine("#%#%#%#%#% We Ran out of Stack Space on thread id : " + GetCurrentThreadId().ToString() + " @ :" + DateTime.Now.ToString() + " #%#%#%#%#%");
            return strBuild.ToString();
        
        Exception inner = ex;
        Enumerable.Range(0, 30).All(x =>
        
            if (x == 0) strBuild.Append("########## Exception begin on thread id : " + GetCurrentThreadId().ToString() + " @ :" + DateTime.Now.ToString() + " ##########\n");
            strBuild.Append("---------------------[" + x.ToString() + "]---------------------\n");
            strBuild.Append("Message : " + inner.Message + "\nStack Trace : " + inner.StackTrace + "\n");
            strBuild.Append("---------------------[" + x.ToString() + "]---------------------\n");
            inner = inner.InnerException;
            if (inner == null)
            
                strBuild.Append("########## Exception End on thread id : " + GetCurrentThreadId().ToString() + " @ :" + DateTime.Now.ToString() + " ##########\n\n");
                return false;
            
            return true;
        );
        return strBuild.ToString();
    

【讨论】:

以上是关于有没有办法在递归调用之前检查可用的堆栈大小? (C#)的主要内容,如果未能解决你的问题,请参考以下文章

找到最大递归深度

如何增加 Rust 库可用的堆栈大小?

在编译时检查堆栈使用情况

递归过多/超出最大调用堆栈大小(Jquery 1.9.1)

超出最大调用堆栈大小 - 没有明显的递归

有没有办法在运行时检测堆栈溢出? (c++)