StackOverflowException は OutOfMemoryException とはまったく異なります。
OOME は、プロセスで使用できるメモリがまったくないことを意味します。これは、新しいスタックで新しいスレッドを作成しようとしたとき、またはヒープ上に新しいオブジェクトを作成しようとしたとき (およびその他のいくつかのケース) に発生する可能性があります。
SOE は、スレッドのスタックを意味します。デフォルトでは 1M ですが、スレッドの作成時または実行可能ファイルのデフォルトが異なる場合は、別の方法で設定できます。したがって、ASP.NET スレッドのデフォルトは 1M ではなく 256k で、使い果たされました。これは、メソッドの呼び出し時、またはローカルの割り当て時に発生する可能性があります。
関数 (メソッドまたはプロパティ) を呼び出すと、呼び出しの引数がスタックに置かれ、関数が戻るときに返されるアドレスがスタックに置かれ、実行は呼び出された関数にジャンプします。次に、いくつかのローカルがスタックに配置されます。関数が引き続き実行されると、さらにいくつかのものが配置される場合があります。stackalloc
また、そうでなければヒープ割り当てが使用されるスタックスペースを明示的に使用します。
その後、別の関数を呼び出すと、同じことが再び起こります。次に、その関数が戻り、実行が格納された戻りアドレスに戻り、スタック内のポインターが元に戻り (スタックに配置された値をクリーンアップする必要はありません。現在は無視されます)、そのスペースが再び利用可能になります。 .
その 1M のスペースを使い切ると、StackOverflowException
. 1M (または 256k) は、このような使用には大量のメモリであるため (実際には大きなオブジェクトをスタックに入れません)、SOE を引き起こす可能性のある 3 つのことは次のとおりです。
stackalloc
そうでないときに使用して最適化するのは良い考えだと誰かが考え、その 1M をすぐに使い果たしました。
- スタックが通常よりも小さいスレッドを作成して最適化するのは良い考えだと誰かが考え、その小さなスタックを使い果たしました。
- 再帰的 (直接または複数のステップを介して) 呼び出しは、無限ループに陥ります。
- 無限ではありませんでしたが、十分な大きさでした。
ケース 4 があります。1 と 2 は非常にまれです (これらのリスクを冒すには、慎重に検討する必要があります)。ケース 3 は群を抜いて最も一般的であり、再帰が無限であってはならないというバグを示していますが、間違いはそうであることを意味します。
皮肉なことに、この場合、反復的ではなく再帰的アプローチを採用したことを喜んでいるはずです。SOE はバグとその場所を明らかにしますが、反復的アプローチではおそらく無限ループが発生してすべてが停止する可能性があります。見つけにくくなります。
ケース 4 では、2 つのオプションがあります。非常にまれなケースですが、呼び出しがわずかに多すぎる場合は、より大きなスタックを持つスレッドで実行できます。これはあなたには当てはまりません。
代わりに、再帰的なアプローチから反復的なアプローチに変更する必要があります。ほとんどの場合、これは非常に難しいことではありません。メソッドは自分自身を再度呼び出す代わりに、ループを使用します。たとえば、階乗法の古典的な教えの例を考えてみましょう。
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
、2 番目に を省略したとします。それぞれに同等のバグがあり、最初に SOE が発生し、2 番目に停止しないプログラムが発生します。
あなたが話しているようなコードの場合、以前の結果に基づいて進むにつれてますます多くの結果を生成し続ける場合、取得した結果をデータ構造に配置できます(両方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 を使用して重複をキャッチする方法を追加することをお勧めします。最初に 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.");
または、重複する文字列が有効で、2 番目の呼び出しの実行をスキップしたい場合:
if(!seen.Add(conceptCKI))
continue;//skip rest of loop, and move on to the next one.