AspNetSynchronizationContext и ждите продолжения в ASP.NET
Я заметил неожиданный (и я бы сказал, избыточный) переключатель потока после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)
или пользовательский ожидающий, но это уберет автоматический поток свойств состояния 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
.
ИМО, было бы более уместно выразить это так:
Task newTask = _lastScheduledTask.ContinueWith(_ => SafeWrapCallback(action),
TaskContinuationOptions.ExecuteSynchronously);
_lastScheduledTask = newTask;
Или, по крайней мере, я бы ожидал, что он будет использоватьTaskScheduler.Default
скорее, чемTaskScheduler.Current
.
Если я включуLegacyAspNetSynchronizationContext
с<add key="aspnet:UseTaskFriendlySynchronizationContext" value="false" />
вweb.config
, он работает так, как нужно: контекст синхронизации устанавливается в потоке, где ожидаемая задача завершилась, и продолжение выполняется там синхронно.