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
Другие вопросы по тегам