Я заметил неожиданный (и, я бы сказал, лишний) потоковый переключатель после await
внутри асинхронного метода контроллера ASP.NET Web API.
Например, ниже я ожидаю увидеть те же самые ManagedThreadId
в местах № 2 и 3 #, но чаще всего я вижу другой поток на # 3:
public class TestController : ApiController
{
public async Task<string> GetData()
{
Debug.WriteLine(new
{
where = "1) before await",
thread = Thread.CurrentThread.ManagedThreadId,
context = SynchronizationContext.Current
});
await Task.Delay(100).ContinueWith(t =>
{
Debug.WriteLine(new
{
where = "2) inside ContinueWith",
thread = Thread.CurrentThread.ManagedThreadId,
context = SynchronizationContext.Current
});
}, TaskContinuationOptions.ExecuteSynchronously); //.ConfigureAwait(false);
Debug.WriteLine(new
{
where = "3) after await",
thread = Thread.CurrentThread.ManagedThreadId,
context = SynchronizationContext.Current
});
return "OK";
}
}
Я посмотрел на реализацию AspNetSynchronizationContext.Post
, по сути это сводится к следующему:
Task newTask = _lastScheduledTask.ContinueWith(_ => SafeWrapCallback(action));
_lastScheduledTask = newTask;
Таким образом, продолжение запланировано на ThreadPool
, а не нарисовано. Здесь ContinueWith
использует TaskScheduler.Current
, который по моему опыту всегда является экземпляром ThreadPoolTaskScheduler
внутри ASP.NET(но это не обязательно, см. Ниже).
Я мог бы устранить избыточный переключатель потока, подобный этому, с помощью ConfigureAwait(false)
или пользовательского awaiter, но это уберет автоматический поток свойств состояния HTTP-запроса, например HttpContext.Current
.
Там есть другой побочный эффект текущей реализации AspNetSynchronizationContext.Post
. Это приводит к взаимоблокировке в следующем случае:
await Task.Factory.StartNew(
async () =>
{
return await Task.Factory.StartNew(
() => Type.Missing,
CancellationToken.None,
TaskCreationOptions.None,
scheduler: TaskScheduler.FromCurrentSynchronizationContext());
},
CancellationToken.None,
TaskCreationOptions.None,
scheduler: TaskScheduler.FromCurrentSynchronizationContext()).Unwrap();
Этот пример, хотя и немного надуманный, показывает, что может произойти, если TaskScheduler.Current
есть TaskScheduler.FromCurrentSynchronizationContext()
, т.е. сделано из AspNetSynchronizationContext
. Он не использует какой-либо код блокировки и был бы выполнен гладко в WinForms или WPF.
Это поведение AspNetSynchronizationContext
отличается от реализации v4.0 (которое все еще существует как LegacyAspNetSynchronizationContext
).
Итак, в чем причина таких изменений? Я подумал, что идея в этом может заключаться в сокращении разрыва для взаимоблокировок, но тупиковая ситуация по-прежнему возможна с текущей реализацией при использовании Task.Wait()
или Task.Result
.
IMO, было бы более целесообразно сделать следующее:
Task newTask = _lastScheduledTask.ContinueWith(_ => SafeWrapCallback(action),
TaskContinuationOptions.ExecuteSynchronously);
_lastScheduledTask = newTask;
Или, по крайней мере, я ожидаю, что он будет использовать TaskScheduler.Default
, а не TaskScheduler.Current
.
Если я включаю LegacyAspNetSynchronizationContext
с <add key="aspnet:UseTaskFriendlySynchronizationContext" value="false" />
в web.config
, он работает по желанию: контекст синхронизации устанавливается в потоке, где ожидаемая задача завершена, и продолжение там синхронно выполняется.