Как предотвратить и / или обработать исключение StackOverflowException?


Я хотел бы либо предотвратить, либо обработать исключение StackOverflowException, которое я получаю от вызова XslCompiledTransform.Метод преобразования в Редакторе Xsl, который я пишу. Проблема, похоже, заключается в том, что пользователь может написать xsl-скрипт, который является бесконечно рекурсивным, и он просто взрывается при вызове метода Transform. (То есть проблема заключается не только в типичной программной ошибке, которая обычно является причиной такого исключения.)

есть ли способ, чтобы обнаружить и / или ограничить, сколько рекурсий разрешено? Или любые другие идеи, чтобы этот код не просто взорвался на мне?

10 66

10 ответов:

От Microsoft:

начиная с платформы .NET Framework версия 2.0, исключение StackOverflowException объект не может быть пойман с помощью try-catch блок и соответствующий процесс прекращена по умолчанию. Следовательно, пользователям рекомендуется написать свой код обнаружение и предотвращение стека переполнение. Например, если ваш применение зависит от рекурсии, использования счетчик или условие состояния для завершения рекурсивного цикла.

Я предполагая, что исключение происходит внутри внутреннего метода .NET, а не в вашем коде.

Вы можете сделать пару вещей.

  • напишите код, который проверяет xsl на бесконечную рекурсию и уведомляет пользователя перед применением преобразования (Ugh).
  • загрузите код XslTransform в отдельный процесс (Hacky, но меньше работы).

вы можете использовать класс Process для загрузки сборки, которая будет применять преобразование в отдельный обработайте и предупредите пользователя о сбое, если он умрет, не убивая ваше основное приложение.

EDIT: я только что протестировал, вот как это сделать:

MainProcess:

// This is just an example, obviously you'll want to pass args to this.
Process p1 = new Process();
p1.StartInfo.FileName = "ApplyTransform.exe";
p1.StartInfo.UseShellExecute = false;
p1.StartInfo.WindowStyle = ProcessWindowStyle.Hidden;

p1.Start();
p1.WaitForExit();

if (p1.ExitCode == 1)    
   Console.WriteLine("StackOverflow was thrown");

ApplyTransform Процесс:

class Program
{
    static void Main(string[] args)
    {
        AppDomain.CurrentDomain.UnhandledException += new UnhandledExceptionEventHandler(CurrentDomain_UnhandledException);
        throw new StackOverflowException();
    }

    // We trap this, we can't save the process, 
    // but we can prevent the "ILLEGAL OPERATION" window 
    static void CurrentDomain_UnhandledException(object sender, UnhandledExceptionEventArgs e)
    {
        if (e.IsTerminating)
        {
            Environment.Exit(1);
        }
    }
}

Примечание вопрос в bounty от @WilliamJockusch и исходный вопрос отличаются.

этот ответ касается StackOverflow в общем случае сторонних библиотек и того, что вы можете/не можете с ними делать. Если вы ищете особый случай с XslTransform, см. принятый ответ.


переполнение стека происходит потому, что данные в стеке превышают определенный предел (в байтах). Подробности как это обнаружение работает можно найти здесь.

мне интересно, есть ли общий способ отслеживать StackOverflowExceptions. Другими словами, предположим, что у меня есть бесконечная рекурсия где-то в моем коде, но я понятия не имею, где. Я хочу отследить его каким-то образом, что проще, чем шагать через код повсюду, пока я не увижу, что это происходит. Мне все равно, насколько это банально.

как я уже упоминал в ссылке, обнаружение стека переполнение из статического анализа кода потребует решения проблемы остановки, которая является неразрешимые. Теперь, когда мы установили, что нет серебряной пули, я могу показать вам несколько трюков, которые, как мне кажется, помогают отследить проблему.

Я думаю, что этот вопрос можно интерпретировать по-разному, и поскольку мне немного скучно: -), я разбью его на разные варианты.

обнаружение переполнения стека в тесте окружающая среда

в основном проблема здесь заключается в том, что у вас есть (ограниченная) тестовая среда и вы хотите обнаружить переполнение стека в (расширенной) производственной среде.

вместо обнаружения самого SO, я решаю это, используя тот факт, что глубина стека может быть установлена. Отладчик предоставит вам всю необходимую информацию. Большинство языков позволяют указать размер стека или максимальную глубину рекурсии.

в основном, я пытаюсь заставить так делая глубину стека как можно меньше. Если он не переполняется, я всегда могу сделать его больше (=в этом случае: безопаснее) для производственной среды. В тот момент, когда вы получаете переполнение стека, вы можете вручную решить, является ли это "допустимым" или нет.

для этого передайте размер стека (в нашем случае: небольшое значение) в параметр потока и посмотрите, что произойдет. Размер стека по умолчанию в .NET составляет 1 МБ, мы будем использовать меньшее значение:

class StackOverflowDetector
{
    static int Recur()
    {
        int variable = 1;
        return variable + Recur();
    }

    static void Start()
    {
        int depth = 1 + Recur();
    }

    static void Main(string[] args)
    {
        Thread t = new Thread(Start, 1);
        t.Start();
        t.Join();
        Console.WriteLine();
        Console.ReadLine();
    }
}

Примечание: мы мы также будем использовать этот код ниже.

как только он переполняется, вы можете установить его на большее значение, пока не получите так, что имеет смысл.

создание исключений перед вами так

The StackOverflowException не улавливается. Это означает, что вы мало что можете сделать, когда это произошло. Поэтому, если вы считаете, что что-то обязательно пойдет не так в вашем коде, вы можете сделать свое собственное исключение в некоторых случаях. Единственное, что вам нужно для этого-это текущий стек глубина; нет необходимости в счетчике, вы можете использовать реальные значения из .NET:

class StackOverflowDetector
{
    static void CheckStackDepth()
    {
        if (new StackTrace().FrameCount > 10) // some arbitrary limit
        {
            throw new StackOverflowException("Bad thread.");
        }
    }

    static int Recur()
    {
        CheckStackDepth();
        int variable = 1;
        return variable + Recur();
    }

    static void Main(string[] args)
    {
        try
        {
            int depth = 1 + Recur();
        }
        catch (ThreadAbortException e)
        {
            Console.WriteLine("We've been a {0}", e.ExceptionState);
        }
        Console.WriteLine();
        Console.ReadLine();
    }
}

обратите внимание, что этот подход также работает, если вы имеете дело со сторонними компонентами, которые используют механизм обратного вызова. Единственное, что требуется, это то, что вы можете перехватить некоторые вызовов в трассировке стека.

обнаружение в отдельном потоке

вы явно предложили это,так что здесь идет этот.

вы можете попробовать обнаружить так в отдельный поток.. но это, вероятно, не принесет вам никакой пользы. Переполнение стека может произойти быстро, даже до того, как вы получите переключатель контекста. Это означает, что этот механизм не является надежным вообще... Я бы не рекомендовал на самом деле использовать его. Это было весело строить, хотя, так что вот код : -)

class StackOverflowDetector
{
    static int Recur()
    {
        Thread.Sleep(1); // simulate that we're actually doing something :-)
        int variable = 1;
        return variable + Recur();
    }

    static void Start()
    {
        try
        {
            int depth = 1 + Recur();
        }
        catch (ThreadAbortException e)
        {
            Console.WriteLine("We've been a {0}", e.ExceptionState);
        }
    }

    static void Main(string[] args)
    {
        // Prepare the execution thread
        Thread t = new Thread(Start);
        t.Priority = ThreadPriority.Lowest;

        // Create the watch thread
        Thread watcher = new Thread(Watcher);
        watcher.Priority = ThreadPriority.Highest;
        watcher.Start(t);

        // Start the execution thread
        t.Start();
        t.Join();

        watcher.Abort();
        Console.WriteLine();
        Console.ReadLine();
    }

    private static void Watcher(object o)
    {
        Thread towatch = (Thread)o;

        while (true)
        {
            if (towatch.ThreadState == System.Threading.ThreadState.Running)
            {
                towatch.Suspend();
                var frames = new System.Diagnostics.StackTrace(towatch, false);
                if (frames.FrameCount > 20)
                {
                    towatch.Resume();
                    towatch.Abort("Bad bad thread!");
                }
                else
                {
                    towatch.Resume();
                }
            }
        }
    }
}

запустить в отладчике и получайте удовольствие от того, что происходит.

используя характеристики переполнения стека

другой интерпретация вашего вопроса: "где находятся фрагменты кода, которые потенциально могут вызвать исключение переполнения стека?". Очевидно, что ответ на это: весь код с рекурсией. Затем для каждого фрагмента кода можно выполнить ручной анализ.

это также можно определить с помощью статического анализа кода. Для этого вам нужно декомпилировать все методы и выяснить, содержат ли они бесконечную рекурсию. Вот некоторый код, который делает это для ты:

// A simple decompiler that extracts all method tokens (that is: call, callvirt, newobj in IL)
internal class Decompiler
{
    private Decompiler() { }

    static Decompiler()
    {
        singleByteOpcodes = new OpCode[0x100];
        multiByteOpcodes = new OpCode[0x100];
        FieldInfo[] infoArray1 = typeof(OpCodes).GetFields();
        for (int num1 = 0; num1 < infoArray1.Length; num1++)
        {
            FieldInfo info1 = infoArray1[num1];
            if (info1.FieldType == typeof(OpCode))
            {
                OpCode code1 = (OpCode)info1.GetValue(null);
                ushort num2 = (ushort)code1.Value;
                if (num2 < 0x100)
                {
                    singleByteOpcodes[(int)num2] = code1;
                }
                else
                {
                    if ((num2 & 0xff00) != 0xfe00)
                    {
                        throw new Exception("Invalid opcode: " + num2.ToString());
                    }
                    multiByteOpcodes[num2 & 0xff] = code1;
                }
            }
        }
    }

    private static OpCode[] singleByteOpcodes;
    private static OpCode[] multiByteOpcodes;

    public static MethodBase[] Decompile(MethodBase mi, byte[] ildata)
    {
        HashSet<MethodBase> result = new HashSet<MethodBase>();

        Module module = mi.Module;

        int position = 0;
        while (position < ildata.Length)
        {
            OpCode code = OpCodes.Nop;

            ushort b = ildata[position++];
            if (b != 0xfe)
            {
                code = singleByteOpcodes[b];
            }
            else
            {
                b = ildata[position++];
                code = multiByteOpcodes[b];
                b |= (ushort)(0xfe00);
            }

            switch (code.OperandType)
            {
                case OperandType.InlineNone:
                    break;
                case OperandType.ShortInlineBrTarget:
                case OperandType.ShortInlineI:
                case OperandType.ShortInlineVar:
                    position += 1;
                    break;
                case OperandType.InlineVar:
                    position += 2;
                    break;
                case OperandType.InlineBrTarget:
                case OperandType.InlineField:
                case OperandType.InlineI:
                case OperandType.InlineSig:
                case OperandType.InlineString:
                case OperandType.InlineTok:
                case OperandType.InlineType:
                case OperandType.ShortInlineR:
                    position += 4;
                    break;
                case OperandType.InlineR:
                case OperandType.InlineI8:
                    position += 8;
                    break;
                case OperandType.InlineSwitch:
                    int count = BitConverter.ToInt32(ildata, position);
                    position += count * 4 + 4;
                    break;

                case OperandType.InlineMethod:
                    int methodId = BitConverter.ToInt32(ildata, position);
                    position += 4;
                    try
                    {
                        if (mi is ConstructorInfo)
                        {
                            result.Add((MethodBase)module.ResolveMember(methodId, mi.DeclaringType.GetGenericArguments(), Type.EmptyTypes));
                        }
                        else
                        {
                            result.Add((MethodBase)module.ResolveMember(methodId, mi.DeclaringType.GetGenericArguments(), mi.GetGenericArguments()));
                        }
                    }
                    catch { } 
                    break;


                default:
                    throw new Exception("Unknown instruction operand; cannot continue. Operand type: " + code.OperandType);
            }
        }
        return result.ToArray();
    }
}

class StackOverflowDetector
{
    // This method will be found:
    static int Recur()
    {
        CheckStackDepth();
        int variable = 1;
        return variable + Recur();
    }

    static void Main(string[] args)
    {
        RecursionDetector();
        Console.WriteLine();
        Console.ReadLine();
    }

    static void RecursionDetector()
    {
        // First decompile all methods in the assembly:
        Dictionary<MethodBase, MethodBase[]> calling = new Dictionary<MethodBase, MethodBase[]>();
        var assembly = typeof(StackOverflowDetector).Assembly;

        foreach (var type in assembly.GetTypes())
        {
            foreach (var member in type.GetMembers(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Static | BindingFlags.Instance).OfType<MethodBase>())
            {
                var body = member.GetMethodBody();
                if (body!=null)
                {
                    var bytes = body.GetILAsByteArray();
                    if (bytes != null)
                    {
                        // Store all the calls of this method:
                        var calls = Decompiler.Decompile(member, bytes);
                        calling[member] = calls;
                    }
                }
            }
        }

        // Check every method:
        foreach (var method in calling.Keys)
        {
            // If method A -> ... -> method A, we have a possible infinite recursion
            CheckRecursion(method, calling, new HashSet<MethodBase>());
        }
    }

теперь тот факт, что цикл метода содержит рекурсию, никоим образом не гарантирует, что произойдет переполнение стека - это просто наиболее вероятное предварительное условие для исключения переполнения стека. Короче говоря, это означает, что этот код будет определять фрагменты кода, где переполнение стека можете происходят, которые должны значительно сузить большинство кода.

пока другие подходы

есть и другие подходы вы можете попробовать, что я не описал здесь.

  1. обработка переполнения стека путем размещения процесса среды CLR и его обработки. Обратите внимание, что вы все еще не можете "поймать" его.
  2. изменение всего кода IL, создание другой DLL, добавление проверок на рекурсию. Да, это вполне возможно (я реализовал это в прошлом : -), это просто сложно и включает в себя много кода, чтобы получить это право.
  3. использовать .Чистый API для профилирования, чтобы отслеживать все вызовы методов и использовать этот рис. из стека переполнения. Например, вы можете реализовать проверки, что если вы сталкиваетесь с тем же методом X раз в дереве вызовов, вы даете сигнал. Есть проект здесь это даст вам фору.

Я бы предложил создать оболочку вокруг объекта XmlWriter, чтобы он подсчитывал количество вызовов WriteStartElement / WriteEndElement, и если вы ограничите количество тегов некоторым числом (например, 100), вы сможете создать другое исключение, например - InvalidOperation.

Это должно решить проблему в большинстве случаев

public class LimitedDepthXmlWriter : XmlWriter
{
    private readonly XmlWriter _innerWriter;
    private readonly int _maxDepth;
    private int _depth;

    public LimitedDepthXmlWriter(XmlWriter innerWriter): this(innerWriter, 100)
    {
    }

    public LimitedDepthXmlWriter(XmlWriter innerWriter, int maxDepth)
    {
        _maxDepth = maxDepth;
        _innerWriter = innerWriter;
    }

    public override void Close()
    {
        _innerWriter.Close();
    }

    public override void Flush()
    {
        _innerWriter.Flush();
    }

    public override string LookupPrefix(string ns)
    {
        return _innerWriter.LookupPrefix(ns);
    }

    public override void WriteBase64(byte[] buffer, int index, int count)
    {
        _innerWriter.WriteBase64(buffer, index, count);
    }

    public override void WriteCData(string text)
    {
        _innerWriter.WriteCData(text);
    }

    public override void WriteCharEntity(char ch)
    {
        _innerWriter.WriteCharEntity(ch);
    }

    public override void WriteChars(char[] buffer, int index, int count)
    {
        _innerWriter.WriteChars(buffer, index, count);
    }

    public override void WriteComment(string text)
    {
        _innerWriter.WriteComment(text);
    }

    public override void WriteDocType(string name, string pubid, string sysid, string subset)
    {
        _innerWriter.WriteDocType(name, pubid, sysid, subset);
    }

    public override void WriteEndAttribute()
    {
        _innerWriter.WriteEndAttribute();
    }

    public override void WriteEndDocument()
    {
        _innerWriter.WriteEndDocument();
    }

    public override void WriteEndElement()
    {
        _depth--;

        _innerWriter.WriteEndElement();
    }

    public override void WriteEntityRef(string name)
    {
        _innerWriter.WriteEntityRef(name);
    }

    public override void WriteFullEndElement()
    {
        _innerWriter.WriteFullEndElement();
    }

    public override void WriteProcessingInstruction(string name, string text)
    {
        _innerWriter.WriteProcessingInstruction(name, text);
    }

    public override void WriteRaw(string data)
    {
        _innerWriter.WriteRaw(data);
    }

    public override void WriteRaw(char[] buffer, int index, int count)
    {
        _innerWriter.WriteRaw(buffer, index, count);
    }

    public override void WriteStartAttribute(string prefix, string localName, string ns)
    {
        _innerWriter.WriteStartAttribute(prefix, localName, ns);
    }

    public override void WriteStartDocument(bool standalone)
    {
        _innerWriter.WriteStartDocument(standalone);
    }

    public override void WriteStartDocument()
    {
        _innerWriter.WriteStartDocument();
    }

    public override void WriteStartElement(string prefix, string localName, string ns)
    {
        if (_depth++ > _maxDepth) ThrowException();

        _innerWriter.WriteStartElement(prefix, localName, ns);
    }

    public override WriteState WriteState
    {
        get { return _innerWriter.WriteState; }
    }

    public override void WriteString(string text)
    {
        _innerWriter.WriteString(text);
    }

    public override void WriteSurrogateCharEntity(char lowChar, char highChar)
    {
        _innerWriter.WriteSurrogateCharEntity(lowChar, highChar);
    }

    public override void WriteWhitespace(string ws)
    {
        _innerWriter.WriteWhitespace(ws);
    }

    private void ThrowException()
    {
        throw new InvalidOperationException(string.Format("Result xml has more than {0} nested tags. It is possible that xslt transformation contains an endless recursive call.", _maxDepth));
    }
}

Если ваше приложение зависит от кода 3d-партии (в xsl-скриптах), то вы должны сначала решить, хотите ли вы защитить от ошибок в них или нет. Если вы действительно хотите защитить, то я думаю, что вы должны выполнить свою логику, которая склонна к внешним ошибкам в отдельных доменах приложений. Ловить StackOverflowException не очень хорошо.

Регистрация вопрос.

у меня был stackoverflow сегодня, и я прочитал некоторые из ваших сообщений и решил помочь сборщику мусора.

раньше у меня был почти бесконечный цикл вроде этого:

    class Foo
    {
        public Foo()
        {
            Go();
        }

        public void Go()
        {
            for (float i = float.MinValue; i < float.MaxValue; i+= 0.000000000000001f)
            {
                byte[] b = new byte[1]; // Causes stackoverflow
            }
        }
    }

вместо этого пусть ресурс работает вне области, как это:

class Foo
{
    public Foo()
    {
        GoHelper();
    }

    public void GoHelper()
    {
        for (float i = float.MinValue; i < float.MaxValue; i+= 0.000000000000001f)
        {
            Go();
        }
    }

    public void Go()
    {
        byte[] b = new byte[1]; // Will get cleaned by GC
    }   // right now
}

это сработало для меня, надеюсь, что это поможет кому-то.

этот ответ для @WilliamJockusch.

Мне интересно, есть ли общий способ отследить StackOverflowExceptions. Другими словами, предположим, что я бесконечен рекурсия где-то в моем коде, но я понятия не имею, где. Я хочу отследить его с помощью некоторых средств, что проще, чем шаг через код повсюду, пока я не увижу, как это происходит. Мне все равно, насколько хакерски это. Например, было бы здорово иметь модуль, который я мог бы активировать, возможно даже из другого потока, который набрал стек глубина и жаловался, если он добрался до уровня, который я считал "слишком высоким." Для например, я мог бы установить "слишком высоко" до 600 кадров, полагая, что если стек был слишком глубоким, это должно быть проблемой. Что-то вроде этого вероятный. Другим примером может быть регистрация каждого 1000-го вызова метода в моем коде для вывода отладки. Шансы, что это будет получить некоторые доказательства оверлоу были бы довольно хорошими, и это, вероятно, не будет взорвать выход тоже плохо. Ключ в том, что он не может включать написание чека везде, где происходит переполнение. Потому что весь проблема в том, что я не знаю, где это. Предпочтительно решение не должно зависеть от того, как выглядит моя среда разработки; т. е., он не должен предполагать, что я использую C# через определенный набор инструментов (например ПРОТИВ.)

похоже, вы хотите услышать некоторые методы отладки, чтобы поймать этот StackOverflow, поэтому я подумал, что поделюсь парой для вас пытаться.

1. дамп памяти.

про: дампы памяти-это верный способ выяснить причину переполнения стека. C# MVP & я работал вместе с устранением неполадок a SO, и он продолжал вести блог об этом здесь.

этот метод является самым быстрым способом отследить проблему.

этот метод не требует, чтобы вы воспроизвели проблемы, выполнив следующие шаги, замеченные в журналах.

Кон: дампы памяти очень большой, и вы должны прикрепить AdPlus/procdump процесс.

2. Аспектно-Ориентированное Программирование.

про: это, вероятно, самый простой способ для вас реализовать код, который проверяет размер стека вызовов из любого метода без написания кода в каждом методе вашего приложения. Есть куча рамки AOP, которые позволяют перехватывать до и после вызова.

расскажет вам методы, которые вызывают переполнение стека.

позволяет проверить StackTrace().FrameCount на входе и выходе из всех методов в вашем приложении.

Кон: это будет иметь влияние на производительность-крючки встроены в IL для каждого метода, и вы не можете действительно "деактивировать" его.

это несколько зависит от вашего набора инструментов среды разработки.

3. Ведение Журнала Активности Пользователей.

неделю назад я пытался выследить несколько трудно воспроизвести проблемы. Я разместил это QA регистрация активности пользователей, телеметрия (и переменные в глобальных обработчиках исключений) . Вывод, к которому я пришел, был очень простым пользователем-регистратором действий, чтобы увидеть, как воспроизвести проблемы в отладчике, когда происходит любое необработанное исключение.

про: вы можете включить или выключить по желанию (т. е. подписка на события).

Отслеживание действий пользователя не требует перехвата каждого метода.

вы можете рассчитывать количество подписанных методов событий тоже гораздо проще, чем с AOP.

файлы журнала относительно малы и сосредоточены на том, какие действия необходимо выполнить для воспроизведения проблемы.

Это может помочь вам понять, как пользователи используют приложение.

Кон: не подходит для службы Windows и я уверен, что есть лучшие инструменты для веб-приложений.

не обязательно расскажу вам методы, которые вызывают переполнение стека.

требует, чтобы вы шаг через журналы вручную воспроизводить проблемы, а не дамп памяти, где вы можете получить его и отладить его сразу.


возможно, вы можете попробовать все методы, которые я упоминал выше, и некоторые из них @atlaste опубликовал и рассказал нам, какие из них вы нашли, были самыми легкими/быстрыми/грязными/наиболее приемлемыми для запуска в среде PROD/и т. д.

в любом случае удачи в отслеживании этого так.

С .NET 4.0 вы можете добавить

судя по всему, кроме запуска другого процесса, похоже, нет никакого способа обработки StackOverflowException. Прежде чем кто-либо еще спросит, я попытался использовать AppDomain, но это не сработало:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Text;
using System.Threading;

namespace StackOverflowExceptionAppDomainTest
{
    class Program
    {
        static void recrusiveAlgorithm()
        {
            recrusiveAlgorithm();
        }
        static void Main(string[] args)
        {
            if(args.Length>0&&args[0]=="--child")
            {
                recrusiveAlgorithm();
            }
            else
            {
                var domain = AppDomain.CreateDomain("Child domain to test StackOverflowException in.");
                domain.ExecuteAssembly(Assembly.GetEntryAssembly().CodeBase, new[] { "--child" });
                domain.UnhandledException += (object sender, UnhandledExceptionEventArgs e) =>
                {
                    Console.WriteLine("Detected unhandled exception: " + e.ExceptionObject.ToString());
                };
                while (true)
                {
                    Console.WriteLine("*");
                    Thread.Sleep(1000);
                }
            }
        }
    }
}

если вы в конечном итоге используете решение отдельного процесса, однако, я бы рекомендовал использовать Process.Exited и Process.StandardOutput и обрабатывать ошибки самостоятельно, чтобы дать вашим пользователям лучший опыт.

@WilliamJockusch, если я правильно понял вашу озабоченность, это невозможно (с математической точки зрения)всегда определить бесконечную рекурсию, как это будет означать, чтобы решить проблема останова. Чтобы решить эту проблему, вам понадобится супер-рекурсивный алгоритм (типа предикаты проб и ошибок например) или машина, которая может hypercompute (пример объясняется в следующий раздел - доступен как просмотр в книги).

с практической точки зрения, вы должны знать:

  • сколько памяти стека вы оставили в данный момент времени
  • сколько стековой памяти потребуется вашему рекурсивному методу в данный момент времени для конкретного вывода.

имейте в виду, что с текущими машинами эти данные чрезвычайно изменчивы из-за многозадачности, и я не слышал о программном обеспечении, которое делает задача.

Дайте мне знать, если что-то непонятно.

вы можете прочитать это свойство каждые несколько звонков, Environment.StackTrace, и если stacktrace превысил определенный порог, который вы задали, вы можете вернуть функцию.

вы также должны попытаться заменить некоторые рекурсивные функции циклами.