Может ли длинная цепочка асинхронного ожидания вызвать большое потребление памяти? (Теоретически)

2

Глядя на этот код:

public async Task<T> ConsumeAsync()
    {
          await a();
          await b();
          await c();
          await d();
          //..
    }

Допустим, что a,b,c,d также имеют вложенные асинхронные ожидания (и так далее)

Async/await POV - для каждого await сохраняется конечный автомат.

Вопрос (теоретический):

Поскольку каждый конечный автомат хранится в памяти, может ли это вызвать большое потребление памяти?
Это может быть неопределенный вопрос, но если существует много государств, кажется неизбежным не задумываться о размерах сохраняемых конечных автоматов.

  • 0
    Обратите внимание, что если они завершаются синхронно (что происходит чаще, чем вы думаете), распределение не происходит - конечный автомат является структурой и упаковывается только в блоки при подготовке к планированию продолжения. И ... в любом случае, он довольно маленький.
  • 0
    @MarcGravell они будут работать синхронно только в том случае, если задача уже завершена к тому времени, когда ее await . Нет?
Показать ещё 7 комментариев
Теги:
async-await

4 ответа

4
Лучший ответ

Async/await POV - для каждого await сохраняется конечный автомат.

Не правда. Компилятор генерирует конечный автомат для каждого async метода. Локальные данные в методе поднимаются в поля конечного автомата. Тело метода (в основном) разбито на оператор switch, причем каждый case соответствует части метода между операторами await. int используется для отслеживания того, какой бит метода был выполнен (т.е. какой case должен быть выполнен следующим).

Ваши методы a(), b() и т.д. Могут иметь свои собственные конечные автоматы или не иметь их (в зависимости от того, помечены они как async или нет). Даже если они это сделают, в вашем примере только один из этих конечных автоматов будет создан одновременно.

SharpLab - отличный ресурс для изучения этого материала. Пример.

  • 0
    Не правда. Каждый раз, когда выполняется команда await, создается новый экземпляр класса конечного автомата.
  • 2
    @TheodorZoulias Пожалуйста, предоставьте некоторые доказательства этого утверждения. Конечный автомат является структурой, и он упаковывается в первый раз здесь , затем блок кэшируется для последующих ожиданий.
Показать ещё 8 комментариев
6

Поскольку каждый конечный автомат хранится в памяти, может ли это вызвать большое потребление памяти?

Очень маловероятно. Каждый конечный автомат будет занимать несколько десятков байтов снаружи.

Так что это будет иметь значение только тогда, когда у вас их очень много. Вложение в действительности не будет причиной этого, но выполнение членов Task[] может.

Но это не совсем новая или другая форма любого другого типа ресурса.

2

Существует дополнительная стоимость, но она относительно небольшая.

Дополнительные расходы по сравнению с обычной функцией:

  • Класс для государственной машины
  • экземпляр этого класса
  • один int для стадии исполнения
  • Экземпляр AsyncTaskMethodBuilder

Кроме того, локальные переменные функции будут преобразованы в поля конечного автомата. Это перемещает некоторую память из стека в кучу.

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

Для этого также есть несколько онлайн-инструментов (например, sharplab.io). См. Результаты декомпиляции тривиальной асинхронной функции.

  • 0
    Декомпиляция асинхронной функции не будет отображать количество занятой памяти.
  • 0
    Это позволит увидеть, какие переменные генерируются для конечного автомата и имеют интуицию на накладные расходы. Я согласен, что у вас не будет точного ответа в байтах
1

Не уверен насчет фактического потребления памяти, но тем не менее; шаблон, который вы описываете, по крайней мере, имеет некоторые другие проблемы.

await вызовет некоторые накладные расходы при хранении данных и переключении контекста синхронизации (если есть).

Так что, как правило, вам следует только await если вам нужно выполнить какую-то хронологически зависимую работу; т.е. выход используется для дальнейшей обработки.

Отсутствие захвата возвращаемого значения после await можно считать запахом кода.

Итак, несколько примеров:

скорее

pivate Task<Foo> SomeWork()
{
    var theTask = ...
    return theTask;
}    

затем

pivate async Task<Foo> SomeWork()
{
    var theTask = ...
    return await theTask;
}    

так как

Звонок тот же:

pivate async Task SomeWrapper()
{
    var result = await someWorkObj.SomeWork();

    //some processing.     
}

а также

скорее

public async Task ConsumeAsync()
{
      Task[] tasks = new { task1, task2, ... };

       await Task.WhenAll(tasks);

      //do things
}

затем

public async Task<T> ConsumeAsync()
{
      await a();
      await b();
      await c();
      await d();
      //..
}

так как

Возможно чрезмерное (вложенное) await которое будет иметь дополнительные издержки. Я постараюсь найти несколько статей на эту тему.

Ещё вопросы

Сообщество Overcoder
Наверх
Меню