Какие накладные расходы ожидают без ввода-вывода
Одним из недостатков асинхронного шаблона в C# 5 является то, что задачи не являются ковариантными, т.е. ITask<out TResult>
Я заметил, что мои разработчики часто делают
return await SomeAsyncMethod();
чтобы обойти это.
Какое влияние окажет на производительность этот ящик? Нет ввода-вывода или потока, который он будет ждать и приведёт к правильному коварианту. Что будет делать асинхронная среда в этом случае? Будет ли какое-либо переключение контекста потока?
редактировать: больше информации, этот код не скомпилируется
public class ListProductsQueryHandler : IQueryHandler<ListProductsQuery, IEnumerable<Product>>
{
private readonly IBusinessContext _context;
public ListProductsQueryHandler(IBusinessContext context)
{
_context = context;
}
public Task<IEnumerable<Product>> Handle(ListProductsQuery query)
{
return _context.DbSet<Product>().ToListAsync();
}
}
beacuse Task не является ковариантным, но добавляет await и приводит его к исправлению IEnumerable вместо List, который возвращает ToListAsync
ConfigureAwait(false)
повсюду в доменном коде не ощущается как жизнеспособное решение, но я, конечно, буду использовать его для своих низкоуровневых методов
public async Task<object> Invoke(Query query)
{
var dtoType = query.GetType();
var resultType = GetResultType(dtoType.BaseType);
var handler = _container.GetInstance(typeof(IQueryHandler<,>).MakeGenericType(dtoType, resultType)) as dynamic;
return await handler.Handle(query as dynamic).ConfigureAwait(false);
}
4 ответа
Более заметная стоимость вашего подхода к решению этой проблемы заключается в том, что если есть SynchronizationContext.Current
вам нужно опубликовать значение и дождаться, пока он не запланирует эту работу. Если контекст занят выполнением другой работы, вы можете подождать некоторое время, когда вам на самом деле ничего не нужно делать в этом контексте.
Этого можно избежать, просто используя ConfigureAwait(false)
, сохраняя при этом метод async
,
Как только вы удалили возможность использования контекста синхронизации, конечный автомат, сгенерированный async
Метод не должен иметь накладных расходов, значительно превышающих то, что вы должны были бы предоставить при явном добавлении продолжения.
Какое влияние окажет на производительность этот ящик?
Почти ничего.
Будет ли какое-либо переключение контекста потока?
Не больше, чем обычно.
Как я опишу в своем блоге, когда await
решает уступить, сначала запишет текущий контекст ( SynchronizationContext.Current
если это не null
в этом случае контекст TaskScheduler.Current
). Затем, когда задача завершается, async
метод возобновляет выполнение в этом контексте.
Другим важным элементом в этом разговоре является то, что продолжения задач выполняются синхронно, если это возможно (опять же, описано в моем блоге). Обратите внимание, что это нигде не задокументировано; это деталь реализации.
ConfigureAwait(false)
везде в доменном коде не чувствуется жизнеспособное решение
Вы можете использовать ConfigureAwait(false)
в вашем доменном коде (и я на самом деле рекомендую это делать по семантическим причинам), но это, вероятно, не повлияет на производительность в этом сценарии. Вот почему...
Давайте рассмотрим, как этот вызов работает в вашем приложении. У вас, несомненно, есть некоторый код начального уровня, который зависит от контекста, например, обработчик нажатия кнопки или действие ASP.NET MVC. Это вызывает рассматриваемый код - код домена, который выполняет "асинхронное приведение". Это в свою очередь вызывает низкоуровневый код, который уже используется ConfigureAwait(false)
,
Если вы используете ConfigureAwait(false)
в коде вашего домена логика завершения будет выглядеть так:
- Задача низкого уровня завершена. Поскольку это, вероятно, основано на вводе / выводе, код завершения задачи выполняется в потоке пула потоков.
- Код завершения задачи низкого уровня возобновляет выполнение кода уровня домена. Поскольку контекст не был захвачен, код уровня домена (приведение) выполняется синхронно в одном и том же потоке пула потоков.
- Код уровня домена достигает конца своего метода, который завершает задачу уровня домена. Этот код завершения задачи все еще выполняется в том же потоке пула потоков.
- Код завершения задачи на уровне домена возобновляет выполнение кода начального уровня. Поскольку начальный уровень требует контекста, код начального уровня ставится в очередь в этот контекст. В приложении пользовательского интерфейса это вызывает переключение потока в поток пользовательского интерфейса.
Если вы не используете ConfigureAwait(false)
в коде вашего домена логика завершения будет выглядеть так:
- Задача низкого уровня завершена. Поскольку это, вероятно, основано на вводе / выводе, код завершения задачи выполняется в потоке пула потоков.
- Код завершения задачи низкого уровня возобновляет выполнение кода уровня домена. Поскольку контекст был захвачен, код уровня домена (приведение) ставится в очередь в этот контекст. В приложении пользовательского интерфейса это вызывает переключение потока в поток пользовательского интерфейса.
- Код уровня домена достигает конца своего метода, который завершает задачу уровня домена. Этот код завершения задачи выполняется в контексте.
- Код завершения задачи на уровне домена возобновляет выполнение кода начального уровня. Начальный уровень требует контекста, который уже присутствует.
Так что это просто вопрос, когда происходит это переключение контекста. Для приложений с пользовательским интерфейсом было бы полезно как можно дольше сохранять небольшие объемы работы в пуле потоков, но для большинства приложений это не снизит производительность, если вы этого не сделаете. Аналогично, для приложений ASP.NET вы можете получить небольшое количество параллелизма бесплатно, если вы сохраняете как можно больше кода вне контекста запроса, но для большинства приложений это не имеет значения.
С этим связаны некоторые накладные расходы, хотя в большинстве случаев переключение контекста не происходит - большая часть затрат приходится на конечный автомат, генерируемый для каждого асинхронного метода с ожиданиями. Основная проблема была бы, если бы что-то мешало продолжению работать синхронно. И поскольку мы говорим об операциях ввода / вывода, накладные расходы, как правило, будут полностью уменьшены фактической операцией ввода / вывода - опять же, главное исключение составляют контексты синхронизации, подобные тем, которые использует Winforms.
Чтобы уменьшить эти издержки, вы можете сделать себе вспомогательный метод:
public static Task<TOut> Cast<TIn, TOut>(this Task<TIn> @this, TOut defaultValue)
where TIn : TOut
{
return @this.ContinueWith(t => (TOut)t.GetAwaiter().GetResult());
}
(defaultValue
Аргумент используется только для вывода типа - к сожалению, вам придется явно выписать возвращаемый тип, но, по крайней мере, вам также не придется вводить тип "input" вручную)
Пример использования:
public class A
{
public string Data;
}
public class B : A { }
public async Task<B> GetAsync()
{
return new B { Data =
(await new HttpClient().GetAsync("http://www.google.com")).ReasonPhrase };
}
public Task<A> WrapAsync()
{
return GetAsync().Cast(default(A));
}
Есть небольшая настройка, которую вы можете попробовать при необходимости, например, используя TaskContinuationOptions.ExecuteSynchronously
, который должен хорошо работать для большинства планировщиков задач.
Там будет конечный автомат, сгенерированный компилятором, который сохраняет текущий SynchornizationContext
при вызове метода и восстанавливает его после await
,
Таким образом, может произойти переключение контекста, например, когда вы вызываете это из потока пользовательского интерфейса, код после await
переключится обратно для запуска в потоке пользовательского интерфейса, что приведет к переключению контекста.
Эта статья может быть полезна Асинхронное программирование - Асинхронная производительность: Понимание затрат на асинхронное и ожидание
В вашем случае это может быть удобно ConfigureAwait(false)
после ожидания, чтобы избежать переключения контекста, однако конечный автомат для async
метод все еще будет сгенерирован. В итоге стоимость await
будет незначительным по сравнению со стоимостью вашего запроса.