10

让我们把它想象成一棵家谱,父亲有孩子,那些孩子有孩子,那些孩子有孩子,等等......所以我有一个递归函数,让父亲使用递归来获取孩子,现在只需打印它们调试输出窗口...但是在某些时候(让它运行一个小时并打印 26000 行之后)它给了我一个 StackOverFlowException。

那么我真的内存不足了吗?嗯?那么我不应该得到“内存不足异常”吗? 在其他帖子上,我发现人们说如果递归调用的数量太多,你可能仍然会得到 SOF 异常......

无论如何,我的第一个想法是将树分解成更小的子树..所以我知道我的根父亲总是有这五个孩子,所以我没有在根传递给它的情况下调用我的方法,而是说好的用根通行证的孩子调用它五次..它帮助我认为..但其中一个仍然很大 - 崩溃时有 26000 行 - 仍然存在这个问题。

应用程序域和在运行时以某种深度创建新进程怎么样? 这有帮助吗?

如何创建我自己的 Stack 并使用它而不是递归方法?这有帮助吗?

这也是我的代码的高级,请看一下,也许这实际上有一些愚蠢的错误导致 SOF 错误:

private void MyLoadMethod(string conceptCKI)
{

// make some script calls to DB, so that moTargetConceptList2 will have Concept-Relations for the current node. 

            // when this is zero, it means its a leaf. 
            int numberofKids = moTargetConceptList2.ConceptReltns.Count();
            if (numberofKids == 0)
                return;
            for (int i = 1; i <= numberofKids; i++)
            {
                oUCMRConceptReltn = moTargetConceptList2.ConceptReltns.get_ItemByIndex(i, false);

                //Get the concept linked to the relation concept
                if (oUCMRConceptReltn.SourceCKI == sConceptCKI)
                {
                    oConcept = moTargetConceptList2.ItemByKeyConceptCKI(oUCMRConceptReltn.TargetCKI, false);
                }
                else
                {
                    oConcept = moTargetConceptList2.ItemByKeyConceptCKI(oUCMRConceptReltn.SourceCKI, false);
                }

                //builder.AppendLine("\t" + oConcept.PrimaryCTerm.SourceString);
                Debug.WriteLine(oConcept.PrimaryCTerm.SourceString);

                MyLoadMethod(oConcept.ConceptCKI);
            }
        }
4

3 回答 3

10

如何创建我自己的 Stack 并使用它而不是递归方法?这有帮助吗?

是的!

当您实例化 a 时,Stack<T>这将存在于堆上并且可以任意增长(直到您耗尽可寻址内存)。

如果您使用递归,则使用调用堆栈。调用堆栈比堆小得多。默认值为每个线程 1 MB 的调用堆栈空间。请注意,这可以更改,但不建议这样做。

于 2012-08-11T21:31:20.870 回答
4

StackOverflowException 与 OutOfMemoryException 完全不同。

OOME 意味着进程根本没有可用的内存。这可能是在尝试使用新堆栈创建新线程时,或者在尝试在堆上创建新对象时(以及其他一些情况)。

SOE 表示线程的堆栈 - 默认情况下为 1M,尽管它可以在线程创建时设置不同,或者如果可执行文件具有不同的默认值;因此 ASP.NET 线程默认为 256k 而不是 1M - 已用尽。这可能是在调用方法或分配本地时。

当你调用一个函数(方法或属性)时,调用的参数放在堆栈上,函数返回时应该返回的地址放在堆栈上,然后执行跳转到被调用的函数。然后一些本地人将被放置在堆栈上。随着功能继续执行,可能会在其上放置更多内容。stackalloc还将显式使用一些堆栈空间,否则将使用堆分配。

然后它调用另一个函数,同样的事情又发生了。然后该函数返回,执行跳转回存储的返回地址,堆栈内的指针向上移动(无需清理堆栈上的值,它们现在被忽略)并且该空间再次可用.

如果你用完那 1M 的空间,你会得到一个StackOverflowException. 因为 1M(甚至 256k)对于这些用途来说是大量内存(我们不会将非常大的对象放入堆栈),所以可能导致 SOE 的三件事是:

  1. 有人认为stackalloc在不使用的时候使用优化是个好主意,他们很快就用完了那 1M。
  2. 有人认为通过创建一个比通常堆栈更小的线程来优化是一个好主意,而实际上他们用完了那个小堆栈。
  3. 递归调用(无论是直接调用还是通过多个步骤)都会陷入无限循环。
  4. 它不是无限的,但它足够大。

你有第 4 种情况。第 1 和第 2 种情况很少见(你需要非常谨慎地冒险)。案例 3 是迄今为止最常见的,它表示递归不应该是无限的错误,但错误意味着它是无限的。

具有讽刺意味的是,在这种情况下,您应该很高兴您采用了递归方法而不是迭代方法 - SOE 揭示了错误及其所在位置,而使用迭代方法,您可能会有一个无限循环,使一切都停止,这可以更难找到。

现在对于案例 4,我们有两个选择。在非常罕见的情况下,我们的调用稍微太多了,我们可以在具有更大堆栈的线程上运行它。这不适用于你。

相反,您需要从递归方法更改为迭代方法。大多数时候,这并不难,认为它可能很繁琐。该方法没有再次调用自身,而是使用循环。例如,考虑阶乘方法的经典教学示例:

private static int Fac(int n)
{
  return n <= 1 ? 1 : n * Fac(n - 1);
}

我们不使用递归,而是使用相同的方法循环:

private static int Fac(int n)
{
  int ret = 1;
  for(int i = 1; i <= n, ++i)
    ret *= i;
  return ret;
}

您可以看到为什么这里的堆栈空间较少。迭代版本也将在 99% 的时间内更快。现在,假设我们不小心调用Fac(n)了第一个,而忽略了++i第二个 - 每个中的等效错误,它会导致第一个中的 SOE 和一个永远不会在第二个中停止的程序。

对于您正在谈论的那种代码,您会根据以前的结果不断产生越来越多的结果,您可以将获得的结果放入数据结构中(Queue<T> 并且Stack<T>两者都适用于很多例)所以代码变成了类似的东西):

private void MyLoadMethod(string firstConceptCKI)
{
  Queue<string> pendingItems = new Queue<string>();
  pendingItems.Enqueue(firstConceptCKI);
  while(pendingItems.Count != 0)
  {
    string conceptCKI = pendingItems.Dequeue();
    // make some script calls to DB, so that moTargetConceptList2 will have Concept-Relations for the current node. 
    // when this is zero, it means its a leaf. 
    int numberofKids = moTargetConceptList2.ConceptReltns.Count();
    for (int i = 1; i <= numberofKids; i++)
    {
        oUCMRConceptReltn = moTargetConceptList2.ConceptReltns.get_ItemByIndex(i, false);

        //Get the concept linked to the relation concept
        if (oUCMRConceptReltn.SourceCKI == sConceptCKI)
        {
            oConcept = moTargetConceptList2.ItemByKeyConceptCKI(oUCMRConceptReltn.TargetCKI, false);
        }
        else
        {
            oConcept = moTargetConceptList2.ItemByKeyConceptCKI(oUCMRConceptReltn.SourceCKI, false);
        }

        //builder.AppendLine("\t" + oConcept.PrimaryCTerm.SourceString);
        Debug.WriteLine(oConcept.PrimaryCTerm.SourceString);

        pendingItems.Enque(oConcept.ConceptCKI);
    }
  }
}

(我还没有完全检查这一点,只是添加了排队而不是递归到您问题中的代码)。

然后,这应该与您的代码或多或少相同,但是是迭代的。希望这意味着它会起作用。请注意,如果您检索的数据有循环,则此代码中可能存在无限循环。在这种情况下,当这个代码用太多东西填满队列时,它会抛出一个异常。您可以调试源数据,也可以使用 aHashSet来避免将已处理的项目排入队列。

编辑:更好地添加如何使用 HashSet 来捕获重复项。首先设置一个 HashSet,这可能只是:

HashSet<string> seen = new HashSet<string>();

或者,如果字符串不区分大小写,则最好使用:

HashSet<string> seen = new HashSet<string>(StringComparison.InvariantCultureIgnoreCase) // or StringComparison.CurrentCultureIgnoreCase if that's closer to how the string is used in the rest of the code.

然后在你去使用这个字符串之前(或者在你去把它添加到队列之前,你有以下之一:

如果不应该发生重复的字符串:

if(!seen.Add(conceptCKI))
  throw new InvalidOperationException("Attempt to use \" + conceptCKI + "\" which was already seen.");

或者如果重复的字符串是有效的,我们只想跳过执行第二次调用:

if(!seen.Add(conceptCKI))
  continue;//skip rest of loop, and move on to the next one.
于 2012-08-11T22:58:44.787 回答
2

我认为您有一个递归环(无限递归),而不是真正的堆栈溢出错误。如果你有更多的堆栈内存 - 你也会得到溢出错误。

为了测试它:

声明一个用于存储可操作对象的全局变量:

private Dictionary<int,object> _operableIds = new Dictionary<int,object>();

...
private void Start()
{
    _operableIds.Clear();
    Recurtion(start_id);
}
...
private void Recurtion(int object_id)
{
   if(_operableIds.ContainsKey(object_id))
      throw new Exception("Have a ring!");
   else
     _operableIds.Add(object_id, null/*or object*/);

   ...
   Recurtion(other_id)
   ...
   _operableIds.Remove(object_id);
}
于 2012-08-11T21:55:24.043 回答