创建新进程是否有助于我遍历一棵大树?

时间:2012-08-11 21:27:49

标签: c# tree-traversal

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

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

无论如何,我的第一个想法就是把树打破成更小的子线。所以我知道我的根父亲总是有这五个孩子的事实,所以不要用root调用我的方法一次传递给它,我说好吧用它传给它五次与它通过它...它帮助我思考..但它们中的一个仍然是如此之大 - 当它崩溃时26000行 - 并且仍然有这个问题。

应用程序域和在某个特定深度的运行时创建新进程如何?这有帮助吗?

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

这里也是我的高级代码,请看一下,也许实际上有一些愚蠢的错误会导致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);
            }
        }

3 个答案:

答案 0 :(得分:10)

  

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

是!

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

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

答案 1 :(得分:4)

StackOverflowException与OutOfMemoryException完全不同。

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

SOE意味着线程的堆栈 - 默认为1M,但是在线程创建中可以设置不同,或者可执行文件具有不同的默认值;因此,ASP.NET线程默认为256k而不是1M - 已经用尽了。这可以是在调用方法或分配本地时。

当你调用一个函数(方法或属性)时,调用的参数被放在堆栈上,函数返回时返回的地址放在堆栈上,然后执行跳转到被调用的函数。然后一些当地人将被放置在堆栈上。当函数继续执行时,可以在其上放置一些。 stackalloc还将明确使用一些堆栈空间,否则将使用堆分配。

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

如果你耗尽了1M的空间,你就得到StackOverflowException。因为1M(甚至256k)是这些用途的大量内存(我们不在堆栈中放置非常大的对象),所以可能导致SOE的三件事情是:

  1. 有人认为使用stackalloc进行优化是个不错的选择,并且它们快速耗尽了1M。
  2. 有人认为通过创建一个小于平常堆栈的线程进行优化是个好主意,而不是这样,并且它们会占用那么小的堆栈。
  3. 递归(无论是直接还是通过几个步骤)调用都会陷入无限循环。
  4. 它不是很无限,但足够大。
  5. 你有案例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); } } } 都适用于很多情况)所以代码变成了类似的东西:

    HashSet

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

    这应该与您的代码大致相同,但迭代。希望这意味着它会起作用。请注意,如果要检索的数据有循环,则此代码中可能存在无限循环。在这种情况下,这个代码在填充队列时会抛出一个异常来处理太多东西。您可以调试源数据,也可以使用HashSet<string> seen = new HashSet<string>(); 来避免对已经处理过的项进行排队。

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

    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.
    

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

    {{1}}

答案 2 :(得分: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);
}