Ответ 1
StackOverflowException сильно отличается от OutOfMemoryException.
OOME означает, что для процесса вообще нет памяти. Это может быть при попытке создать новый поток с новым стеком или при попытке создать новый объект в куче (и в нескольких других случаях).
SOE означает, что стек потоков - по умолчанию 1M, хотя он может быть установлен по-разному при создании потока или если исполняемый файл имеет другой умолчаний; поэтому потоки ASP.NET имеют 256k по умолчанию, а не 1M - были исчерпаны. Это может быть вызвано методом или назначением локального.
Когда вы вызываете функцию (метод или свойство), аргументы вызова помещаются в стек, адрес, возвращаемый функцией, когда он возвращается, помещается в стек, а затем выполнение переходит к вызываемой функции. Затем некоторые локальные жители будут помещены в стек. На нее можно поместить еще несколько функций, поскольку функция продолжает выполняться. stackalloc
также будет явно использовать некоторое пространство стека, в котором будет использоваться распределение кучи.
Затем он вызывает другую функцию, и то же самое происходит снова. Затем эта функция возвращается, и выполнение возвращается к сохраненному обратному адресу, а указатель внутри стека перемещается назад (нет необходимости очищать значения, помещенные в стек, они просто игнорируются сейчас), и это пространство снова доступно.
Если вы используете это 1M пространства, вы получаете StackOverflowException
. Поскольку 1M (или даже 256k) - это большой объем памяти для такого использования (мы не помещаем действительно большие объекты в стек), три вещи, которые могут вызвать SOE:
- Кто-то подумал, что было бы неплохо оптимизировать, используя
stackalloc
, когда это было не так, и они быстро использовали этот 1M. - Кто-то подумал, что было бы хорошей идеей оптимизировать, создав поток с меньшим, чем обычно, стекю, когда это было не так, и они используют этот маленький стек.
- Рекурсивный (прямо или через несколько шагов) вызов попадает в бесконечный цикл.
- Это было не совсем бесконечно, но оно было достаточно большим.
У вас есть случай 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 для поиска дубликатов. Сначала настройте HashSet, это может быть просто:
HashSet<string> seen = new HashSet<string>();
Или, если строки используются без учета регистра, вам будет лучше:
HashSet<string> seen = new HashSet<string>(StringComparison.InvariantCultureIgnoreCase) // or StringComparison.CurrentCultureIgnoreCase if that 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.