Глядя на этот код:
public async Task<T> ConsumeAsync()
{
await a();
await b();
await c();
await d();
//..
}
Допустим, что a,b,c,d
также имеют вложенные асинхронные ожидания (и так далее)
Async/await POV - для каждого await
сохраняется конечный автомат.
Вопрос (теоретический):
Поскольку каждый конечный автомат хранится в памяти, может ли это вызвать большое потребление памяти?
Это может быть неопределенный вопрос, но если существует много государств, кажется неизбежным не задумываться о размерах сохраняемых конечных автоматов.
Async/await POV - для каждого await сохраняется конечный автомат.
Не правда. Компилятор генерирует конечный автомат для каждого async
метода. Локальные данные в методе поднимаются в поля конечного автомата. Тело метода (в основном) разбито на оператор switch
, причем каждый case
соответствует части метода между операторами await
. int
используется для отслеживания того, какой бит метода был выполнен (т.е. какой case
должен быть выполнен следующим).
Ваши методы a()
, b()
и т.д. Могут иметь свои собственные конечные автоматы или не иметь их (в зависимости от того, помечены они как async
или нет). Даже если они это сделают, в вашем примере только один из этих конечных автоматов будет создан одновременно.
SharpLab - отличный ресурс для изучения этого материала. Пример.
Поскольку каждый конечный автомат хранится в памяти, может ли это вызвать большое потребление памяти?
Очень маловероятно. Каждый конечный автомат будет занимать несколько десятков байтов снаружи.
Так что это будет иметь значение только тогда, когда у вас их очень много. Вложение в действительности не будет причиной этого, но выполнение членов Task[]
может.
Но это не совсем новая или другая форма любого другого типа ресурса.
Существует дополнительная стоимость, но она относительно небольшая.
Дополнительные расходы по сравнению с обычной функцией:
Кроме того, локальные переменные функции будут преобразованы в поля конечного автомата. Это перемещает некоторую память из стека в кучу.
Я рекомендую декомпилировать простую асинхронную функцию, чтобы увидеть сгенерированный конечный автомат и понять, чего ожидать.
Для этого также есть несколько онлайн-инструментов (например, sharplab.io). См. Результаты декомпиляции тривиальной асинхронной функции.
Не уверен насчет фактического потребления памяти, но тем не менее; шаблон, который вы описываете, по крайней мере, имеет некоторые другие проблемы.
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
которое будет иметь дополнительные издержки. Я постараюсь найти несколько статей на эту тему.
await
. Нет?