Я заметил неожиданный (и, я бы сказал, лишний) потоковый переключатель после 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, он работает по желанию: контекст синхронизации устанавливается в потоке, где ожидаемая задача завершена, и продолжение там синхронно выполняется.