Можно ли рекурсивно вызвать асинхронную функцию, не переполняя стек?
Поскольку возвращаемый сайт асинхронной функции не является вызывающим, я предполагаю, что это работает, но решил проверить, что это безопасно на всякий случай. Если это не так, то почему это переполняет стек?
static async Task CheckAsync(TimeSpan recursiveTimer)
{
// do some work
await Task.Delay(recursiveTimer);
CheckAsync(recursiveTimer);
}
Править:
Я решил просто попробовать его - похоже, он не переполняет стек (сейчас он работает на моей машине - в настоящее время он находится на вызове 210 000). Моя предполагаемая причина заключается в том, что возвращаемый сайт функции CheckAsync на самом деле не CheckAsync, а где-то в асинхронная сантехника. Поэтому, когда CheckAsync вызывает CheckAsync, он фактически не добавляет в стек вызовов через обычный механизм вызова функции, а вместо этого помещает функцию в качестве объекта в некоторую очередь async "для выполнения", которая выполняется через некоторый другой поток, управляющий асинхронными функциями.
Для любого, кто хорошо знает этот механизм: звучит ли это правильно?
2 ответов:
Причина, по которой он работает для вас, заключается не в том, как называется
CheckAsync, а в том, что вы ожидаете результатаTask.Delay. Это всегда будет возвращать" еще не завершенную " задачу, поэтому ожидание ее запланирует продолжение. Это продолжение будет запущено в фактически пустом стеке, поэтому не имеет значения, что вы затем сделаете рекурсивный вызов.Теперь я думаю, что у вас все еще есть утечка памяти, потому что IIRC фреймворк будет отслеживать "логический стек", который будет становись все больше и больше... но это будет храниться в куче и расширяться, пока у вас не закончится память.
Если вы хотите, чтобы стек взорвался, все, что вам нужно сделать, это изменить код на:
static async Task CheckAsync(TimeSpan recursiveTimer) { // Whatever await Task.FromResult(5); CheckAsync(recursiveTimer); }В этот момент, предполагая, что код в "whatever" ничего не ожидает, вы получите полностью синхронный код, просто используя
Taskдля отслеживания завершения и исключений.Я, конечно, не рекомендовал бы это в качестве шаблона для многократного выполнения работы (отчасти из-за памяти утечка, о которой я упоминал), но я надеюсь, что это объясняет, почему вы не получаете переполнение стека.
Причина, по которой он переполняется, заключается в том, что переполняет стек.
10 static async Task CheckAsync(TimeSpan recursiveTimer) 20 { 30 // do some work 40 await Task.Delay(recursiveTimer); 50 CheckAsync(recursiveTimer); 60 }Выполнение кода будет идти
10 20 30 40 50 60 //CheckAsync(recursiveTimer) 10 20 30 40 50 60 //CheckAsync(CheckAsync(recursiveTimer)) 10 20 30 40 50 60 //CheckAsync(CheckAsync(CheckAsync(recursiveTimer))Содержание этого метода должно быть примерно таким:
while(doCheck){ await Task.Delay(recursiveTimer); CheckAsync(recursiveTimer); }Предполагая, что вы хотите ждать после каждого асинхронного вызова и делать это, пока doCheck истинно. Я предполагаю, что вы измените значениеdoCheck в другом потоке.
Взгляните на: лучшие практики асинхронного программирования
Comments