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

Boh*_*ohn 10 c# tree-traversal

让我们把它想象成一个家谱,一个父亲有孩子,孩子有孩子,孩子有孩子等...所以我有一个递归函数,让父亲使用递归来获得孩子,现在只需打印他们调试输出窗口...但在某些时候(让它运行并打印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);
            }
        }
Run Code Online (Sandbox Code Playgroud)

Mar*_*ers 10

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

是!

当你实例化它时,Stack<T>它会在堆上生存并且可以任意增大(直到你用完可寻址的内存).

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

  • @BDotA使用内置的C#数据类型Stack <T>.首先将树的根推入堆栈.然后,执行一个while循环(而堆栈仍包含项目).如果堆栈顶部的项目有子项,请将它们全部推入堆栈.如果堆栈顶部的项目包含零个孩子,请将其弹出.继续,直到堆栈包含0个项目(所有项目都已处理/计算) (7认同)

Jon*_*nna 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);
}
Run Code Online (Sandbox Code Playgroud)

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

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

您可以看到为什么这里的堆栈空间较少。迭代版本在 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);
    }
  }
}
Run Code Online (Sandbox Code Playgroud)

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

这应该与您的代码或多或少相同,但是是迭代的。希望这意味着它会起作用。请注意,如果您检索的数据有循环,则此代码中可能存在无限循环。在这种情况下,当队列中填充了过多的内容而无法处理时,此代码将引发异常。您可以调试源数据,或使用 aHashSet来避免将已处理的项目排队。

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

HashSet<string> seen = new HashSet<string>();
Run Code Online (Sandbox Code Playgroud)

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

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.
Run Code Online (Sandbox Code Playgroud)

然后,在您使用该字符串之前(或者可能在您将其添加到队列之前),您需要执行以下操作之一:

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

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

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

if(!seen.Add(conceptCKI))
  continue;//skip rest of loop, and move on to the next one.
Run Code Online (Sandbox Code Playgroud)