Рекурсия и ожидание /async Ключевые слова

У меня есть хрупкое понимание того, как работает ключевое слово await, и я хочу немного рассказать о нем.

Проблема, которая все еще заставляет вращаться, - это использование рекурсии. Вот пример:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace TestingAwaitOverflow
{
    class Program
    {
        static void Main(string[] args)
        {
            var task = TestAsync(0);
            System.Threading.Thread.Sleep(100000);
        }

        static async Task TestAsync(int count)
        {
            Console.WriteLine(count);
            await TestAsync(count + 1);
        }
    }
}

Этот явно бросает a StackOverflowException.

Я понимаю, что код действительно работает синхронно до первого асинхронного действия, после чего он возвращает объект Task, содержащий информацию об асинхронной операции. В этом случае нет асинхронной операции, поэтому она просто рекурсирует под ложным обещанием, что в итоге она вернет Task.

Теперь изменим его чуть-чуть:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace TestingAwaitOverflow
{
    class Program
    {
        static void Main(string[] args)
        {
            var task = TestAsync(0);
            System.Threading.Thread.Sleep(100000);
        }

        static async Task TestAsync(int count)
        {
            await Task.Run(() => Console.WriteLine(count));
            await TestAsync(count + 1);
        }
    }
}

Это не бросает StackOverflowException. Я могу разобраться, почему это работает, но я бы назвал это скорее чувством кишки (он, вероятно, имеет дело с тем, как код организован для использования обратных вызовов, чтобы избежать создания стека, но я не могу перевести это ощущение кишки в объяснение)

У меня есть два вопроса:

  • Как вторая партия кода избегает StackOverflowException?
  • Предоставляет ли вторая партия кода другие ресурсы? (например, он выделяет абсурдно большое количество объектов Task в куче?)

Спасибо!

Ответ 1

Часть до первого ожидания в любой функции выполняется синхронно. В первом случае из-за этого происходит переполнение стека - нет ничего, что прерывает функцию, вызывающую себя.

Первое ожидание (которое не завершается немедленно - это случай для вас с высокой вероятностью) заставляет функцию возвращаться (и отказываться от своего пространства стека!). Он ставит в очередь все остальное как продолжение. TPL гарантирует, что продолжения никогда не закладываются слишком глубоко. Если существует риск, продолжение ставится в очередь на пул потоков, перезагружая стек (который начал заполняться).

Второй пример может все еще переполняться! Что делать, если задача Task.Run всегда завершена? (Это маловероятно, но возможно при правильном планировании потоков ОС). Затем асинхронная функция никогда не прерывается (заставляя ее возвращать и освобождать все пространство стека), и такое же поведение, как и в случае 1.

Ответ 2

В первом и втором примерах TestAsync все еще ожидает возврата вызова к себе. Разница заключается в том, что рекурсия печатает и возвращает поток для другой работы во втором методе. Поэтому рекурсия не настолько быстра, чтобы быть переполнением стека. Тем не менее, первая задача все еще ждет, и в конечном счете счетчик достигнет максимального значения максимального размера или. Дело в том, что вызывающий поток возвращается, но фактический метод async запланирован в том же потоке. В принципе, метод TestAsync забывается до тех пор, пока ожидание не будет завершено, но оно все еще сохраняется в памяти. Потоку разрешено делать другие вещи до тех пор, пока не наступит завершение, а затем этот поток запомнится и закончится там, где он остановился. Дополнительные ожидающие вызовы сохраняют поток и забывают его снова, пока ожидание не будет завершено. Пока все ожидания не завершены, и поэтому метод завершает работу TaskAsync, все еще находится в памяти. Итак, вот что. Если я скажу метод, чтобы что-то сделать, а затем позвоните в ожидании задания. Остальные мои коды в другом месте продолжают работать. Когда ожидание завершено, код забирается туда и заканчивается, а затем возвращается к тому, что он делал в то время прямо перед ним. В ваших примерах ваш TaskAsync всегда находится в состоянии памяти (так сказать) до тех пор, пока последний вызов не завершится и не вернет вызовы в цепочку.

РЕДАКТОР: Я продолжал говорить, что храню нить или этот поток, и я имел в виду подпрограмму. Все они находятся в одной и той же теме, которая является основным потоком в вашем примере. Извините, если я вас смутил.