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

Ответы на вопрос(2)

Ваш ответ на вопрос