Web API - ConfigureAwait(true) не работает, как я думал
У меня возникли проблемы с пониманием входных и выходных данных "continueOnCapturedContext" с точки зрения.NET v4.6 WebAPI 2.
Проблема, с которой я столкнулся, заключается в том, что между ConfigureAwait(true) и ConfigureAwait(false) нет никакой разницы.
Я собрал пример приложения, которое демонстрирует, что происходит:
public async Task<IHttpActionResult> Get(bool continueOnContext)
{
int beforeRunningExampleThreadId = Thread.CurrentThread.ManagedThreadId;
int runningExampleThreadId = await ExecuteExampleAsync(continueOnContext).ConfigureAwait(continueOnContext);
int afterRunningExampleThreadId = Thread.CurrentThread.ManagedThreadId;
return Ok(new
{
HasSyncContext = SynchronizationContext.Current != null,
ContinueOnCapturedContext = continueOnContext,
BeforeRunningExampleThreadId = beforeRunningExampleThreadId,
RunningExampleThreadId = runningExampleThreadId,
AfterRunningExampleThreadId = afterRunningExampleThreadId,
ResultingCulture = Thread.CurrentThread.CurrentCulture,
SameThreadRunningAndAfter = runningExampleThreadId == afterRunningExampleThreadId
});
}
private async Task<int> ExecuteExampleAsync(bool continueOnContext)
{
return await Task.Delay(TimeSpan.FromMilliseconds(10)).ContinueWith((task) => Thread.CurrentThread.ManagedThreadId).ConfigureAwait(continueOnContext);
}
Для "/Test? ContinueOnContext = true" это возвращает меня:
{"HasSyncContext":true,"ContinueOnCapturedContext":true,"BeforeRunningExampleThreadId":43,"RunningExampleThreadId":31,"AfterRunningExampleThreadId":56,"ResultingCulture":"fr-CA","SameThreadRunningAndAfter":false}
Таким образом, вы можете видеть, что у меня есть контекст Sync, я делаю ConfigureAwait(true), и тем не менее поток не "продолжается" в любом случае - новый поток назначается до, во время выполнения и после выполнения асинхронного кода. Это работает не так, как я ожидал - есть ли здесь какое-то фундаментальное недоразумение?
Может кто-нибудь объяснить мне, почему в этом коде ConfigureAwait(true) и ConfigureAwait(false) эффективно делают одно и то же?
ОБНОВЛЕНИЕ - Я понял это и ответил ниже. Мне также нравится ответ от @YuvalShap. Если вы застряли на этом, как я, я предлагаю вам прочитать оба.
2 ответа
Когда асинхронный обработчик возобновляет выполнение в устаревшем ASP.NET, продолжение ставится в очередь в контексте запроса. Продолжение должно ждать любых других продолжений, которые уже были поставлены в очередь (одновременно может выполняться только одно продолжение). Когда он готов к запуску, поток берется из пула потоков, входит в контекст запроса и затем возобновляет выполнение обработчика. Этот "повторный вход" в контекст запроса включает в себя ряд вспомогательных задач, таких как установка HttpContext.Current и идентификация и культура текущего потока.
Из ASP.NET Core SynchronizationContext Сообщение в блоге Стивена Клири.
Подводя итог, версии ASP.NET до Core использует AspNetSynchronizationContext
в качестве контекста запроса, это означает, что когда вы звоните ConfigureAwait(true)
(или не звонит ConfigureAwait(false)
) вы фиксируете контекст, который указывает методу возобновить выполнение в контексте запроса. Контекст запроса сохраняет HttpContext.Current и идентичность и культуру текущего потока согласованными, но он не является исключительным для конкретного потока, единственное ограничение состоит в том, что одновременно может выполняться только один поток в контексте.
Хорошо, я понял это, поэтому я опубликую ответ на случай, если он поможет другим.
В.NET 4.6 WebAPI 2 - "Захваченный контекст", на котором мы продолжаем, - это не поток, а контекст запроса. Помимо прочего, контекст запроса знает о HttpContext. Когда указано ConfigureAwait (true), мы говорим.NET, что мы хотим сохранить контекст запроса и все о нем (HttpContext и некоторые другие свойства) после ожидания - мы хотим вернуться к контексту, с которого мы начали - это не учитывает поток.
Когда мы указываем ConfigureAwait(false), мы говорим, что нам не нужно возвращаться в контекст запроса, с которого мы начали. Это означает, что.NET может просто вернуться обратно, не заботясь о HttpContext и некоторых других свойствах, следовательно, о предельном выигрыше в производительности.
Учитывая это знание, я изменил свой код:
public async Task<IHttpActionResult> Get(bool continueOnContext)
{
var beforeRunningValue = HttpContext.Current != null;
var whileRunningValue = await ExecuteExampleAsync(continueOnContext).ConfigureAwait(continueOnContext);
var afterRunningValue = HttpContext.Current != null;
return Ok(new
{
ContinueOnCapturedContext = continueOnContext,
BeforeRunningValue = beforeRunningValue,
WhileRunningValue = whileRunningValue,
AfterRunningValue = afterRunningValue,
SameBeforeAndAfter = beforeRunningValue == afterRunningValue
});
}
private async Task<bool> ExecuteExampleAsync(bool continueOnContext)
{
return await Task.Delay(TimeSpan.FromMilliseconds(10)).ContinueWith((task) =>
{
var hasHttpContext = HttpContext.Current != null;
return hasHttpContext;
}).ConfigureAwait(continueOnContext);
}
Когда continueOnContext = true:{"ContinueOnCapturedContext":true,"BeforeRunningValue":true,"WhileRunningValue":false,"AfterRunningValue":true,"SameBeforeAndAfter":true}
Когда continueOnContext = false:{"ContinueOnCapturedContext":false,"BeforeRunningValue":true,"WhileRunningValue":false,"AfterRunningValue":false,"SameBeforeAndAfter":false}
Таким образом, из этого примера вы можете видеть, что HttpContext.Current существует до асинхронного метода и теряется во время асинхронного метода независимо от параметра ConfigureAwait.
Разница заключается в ПОСЛЕ того, как асинхронная операция завершена:
- Когда мы указываем ConfigureAwait (true), мы возвращаемся к контексту запроса, который вызвал асинхронный метод - это делает некоторую служебную работу и синхронизирует HttpContext, поэтому он не равен нулю, когда мы продолжаем
- Когда мы указываем ConfigureAwait(false), мы просто продолжаем, не возвращаясь к контексту запроса, поэтому HttpContext имеет значение null