Подтвердить что ты не робот

ConfigureAwait (false) для запросов верхнего уровня

Я пытаюсь выяснить, следует ли использовать ConfigureAwait (false) для запросов верхнего уровня. Читая этот пост от нескольких авторитетов субъекта: http://blog.stephencleary.com/2012/07/dont-block-on-async-code.html

... он рекомендует что-то вроде этого:

public async Task<JsonResult> MyControllerAction(...)
{
    try
    {
        var report = await _adapter.GetReportAsync();
        return Json(report, JsonRequestBehavior.AllowGet);
    }
    catch (Exception ex)
    {
        return Json("myerror", JsonRequestBehavior.AllowGet);  // really slow without configure await
    }
}

public async Task<TodaysActivityRawSummary> GetReportAsync()
{           
    var data = await GetData().ConfigureAwait(false);

    return data
}

... он говорит об использовании ConfigureAwait (false) для каждого ожидания, кроме вызова верхнего уровня.. Однако при выполнении этого моего исключения требуется несколько секунд, чтобы вернуться к вызывающему и использовать его, и это и сразу же он вернулся.

Какова наилучшая практика для действий контроллера MVC, вызывающих методы async? Должен ли я использовать ConfigureAwait в самом контроллере или просто в вызовах службы, которые используются для запроса данных и т.д.? Если я не использую его при вызове верхнего уровня, ожидая несколько секунд для исключения, кажется проблематичным. Мне не нужен HttpContext, и я видел другие сообщения, которые всегда использовали ConfigureAwait (false), если вам не нужен контекст.

Update: Мне не хватало ConfigureAwait (false) где-то в моей цепочке вызовов, из-за чего исключение не возвращалось сразу же. Однако до сих пор остается открытым вопрос о том, следует ли использовать ConfigureAwait (false) на верхнем уровне.

4b9b3361

Ответ 1

Это веб-сайт с высоким трафиком? Одно из возможных объяснений может заключаться в том, что вы испытываете голод ThreadPool, когда вы не используете ConfigureAwait(false). Без ConfigureAwait(false) продолжение await ставится в очередь через AspNetSynchronizationContext.Post, реализация которого сводится к this:

Task newTask = _lastScheduledTask.ContinueWith(_ => SafeWrapCallback(action));
_lastScheduledTask = newTask; // the newly-created task is now the last one

Здесь ContinueWith используется без TaskContinuationOptions.ExecuteSynchronously (я бы предположил, чтобы сделать продолжения действительно асинхронными и уменьшить вероятность для условий с низким стеком). Таким образом, он получает пустой поток из ThreadPool для продолжения продолжения. Теоретически это может быть тот же самый поток, где завершена задача для await, но, скорее всего, это будет другой поток.

В этот момент, если пул потоков ASP.NET голодает (или должен расти, чтобы удовлетворить новый запрос потока), вы можете испытывать задержку. Стоит упомянуть, что пул потоков состоит из двух под-пулов: потоков IOCP и рабочих потоков (проверьте this и this для некоторых дополнительных деталей). Ваши операции GetReportAsync, скорее всего, будут завершены в подпункте потоков IOCP, который, похоже, не голодает. OTOH, продолжение ContinueWith выполняется в подпункте рабочего потока, который, кажется, голодает в вашем случае.

Это не произойдет, если ConfigureAwait(false) используется полностью. В этом случае все продолжения await будут выполняться синхронно в тех же потоках, которые завершили соответствующие антецедентные задачи, будь то IOCP или рабочие потоки.

Вы можете сравнить использование потоков для обоих сценариев с и без ConfigureAwait(false). Я ожидаю, что это число будет больше, если ConfigureAwait(false) не используется:

catch (Exception ex)
{
    Log("Total number of threads in use={0}",
        Process.GetCurrentProcess().Threads.Count);
    return Json("myerror", JsonRequestBehavior.AllowGet);  // really slow without configure await
}

Вы также можете попытаться увеличить размер пула потоков ASP.NET(для целей диагностики, а не для окончательного решения), чтобы увидеть, действительно ли описанный сценарий имеет место здесь:

<configuration>
  <system.web>
    <applicationPool 
        maxConcurrentRequestsPerCPU="6000"
        maxConcurrentThreadsPerCPU="0" 
        requestQueueLimit="6000" />
  </system.web>
</configuration>

Обновлено для комментариев:

Я понял, что у меня отсутствует ContinueAwait где-то в моей цепочке. Теперь это отлично работает при выдаче исключения, даже если верхний уровень не используйте ConfigureAwait (false).

Это говорит о том, что ваш код или используемая сторонняя библиотека могут использовать блокирующие конструкции (Task.Result, Task.Wait, WaitHandle.WaitOne, возможно, с некоторой добавленной логикой тайм-аута). Вы искали их? Попробуйте предложение Task.Run в нижней части этого обновления. Кроме того, я все равно проведу проверку количества потоков, чтобы исключить голодание/заикание пула потоков.

Итак, вы говорите, что если я использую ContinueAwait даже на верхнем уровне Я теряю все преимущества async?

Нет, я не говорю этого. Вся точка async заключается в том, чтобы избежать блокировки потоков в ожидании чего-либо, и эта цель достигается независимо от добавленного значения ContinueAwait(false).

Я хочу сказать, что не использовать ConfigureAwait(false) может привести к избыточному переключению контекста (что обычно означает переключение потоков), что может быть проблемой в ASP.NET, если пул потоков работает в своем качестве. Тем не менее, избыточный переключатель потоков по-прежнему лучше, чем заблокированный поток, с точки зрения масштабируемости сервера.

Справедливости ради, с использованием ContinueAwait(false) может также вызвать избыточное переключение контекста, особенно если оно использовалось непоследовательно по цепочке вызовов.

Тем не менее, ContinueAwait(false) также часто используется неправильно как средство против тупиков, вызванное блокирование асинхронного кода. Вот почему я предложил выше искать эту блокирующую конструкцию во всей базе кода.

Однако до сих пор остается открытым вопрос о том, ConfigureAwait (false) следует использовать на верхнем уровне.

Я надеюсь, что Стивен Клири сможет лучше разобраться в этом, здесь мои мысли.

Всегда есть код "супер-верхнего уровня", который вызывает ваш код верхнего уровня. Например, в случае приложения пользовательского интерфейса он представляет собой код рамки, который вызывает обработчик события async void. В случае ASP.NET это асинхронный контроллер BeginExecute. Ответственность за этот код супер-верхнего уровня лежит на том, что после завершения асинхронной задачи продолжения (если они есть) выполняются в правильном контексте синхронизации. Это не ответственность за код вашей задачи. Например, не может быть никаких продолжений вообще, например, с обработчиком событий fire-and-forget async void; почему вы хотите восстановить контекст внутри такого обработчика?

Таким образом, внутри ваших методов верхнего уровня, если вы не заботитесь о контексте для продолжений await, используйте ConfigureAwait(false), как только сможете.

Кроме того, если вы используете стороннюю библиотеку, которая, как известно, является агностикой контекста, но все же может использовать ConfigureAwait(false) непоследовательно, вы можете обернуть вызов с помощью Task.Run или что-то вроде WithNoContext. Вы сделали бы это, чтобы получить цепочку асинхронных вызовов от контекста заранее:

var report = await Task.Run(() =>
    _adapter.GetReportAsync()).ConfigureAwait(false);
return Json(report, JsonRequestBehavior.AllowGet);

Это добавит один дополнительный переключатель потока, но может сэкономить вам больше, если ConfigureAwait(false) используется непоследовательно внутри GetReportAsync или любого из его дочерних вызовов. Это также послужило бы обходным путем для возможных взаимоблокировок, вызванных этими блокирующими конструкциями внутри цепочки вызовов (если они есть).

Обратите внимание, однако, что в ASP.NET HttpContext.Current не является единственным статическим свойством, которое течет с помощью AspNetSynchronizationContext. Например, там также Thread.CurrentThread.CurrentCulture. Убедитесь, что вы действительно не заботитесь о потере контекста.

Обновлено, чтобы ответить на комментарий:

Для точек коричневого цвета, может быть, вы можете объяснить эффекты ConfigureAwait (false)... Какой контекст не сохраняется. Это просто HttpContext или локальные переменные объекта класса и т.д.?

Все локальные переменные метода async сохраняются в await, а также неявная this ссылка - по дизайну. Они фактически захватываются в структуру машинного состояния async, созданного компилятором, поэтому технически они не находятся в текущем стеке потоков. В некотором смысле это похоже на то, как делегат С# захватывает локальные переменные. Фактически, обратный вызов продолжения await сам по себе является делегатом, переданным в ICriticalNotifyCompletion.UnsafeOnCompleted (реализуемый ожидаемым объектом; для Task он TaskAwaiter; ConfigureAwait, ConfiguredTaskAwaitable).

OTOH, большая часть глобального состояния (статические/TLS-переменные, статические свойства класса) не автоматически перетекает через ожидания. То, что происходит, зависит от конкретного контекста синхронизации. В отсутствие одного (или когда используется ConfigureAwait(false)) единственное глобальное состояние, с которым сохраняется, - это то, что получает поток ExecutionContext. Microsoft Stephen Toub имеет отличную должность: "ExecutionContext vs SynchronizationContext" . Он упоминает SecurityContext и Thread.CurrentPrincipal, что имеет решающее значение для безопасности. Кроме этого, я не знаю ни одного официально задокументированного и полного списка глобальных свойств состояния, которые течет ExecutionContext.

Вы можете заглянуть в ExecutionContext.Capture source, чтобы узнать больше о том, что именно происходит, но вы не должны зависеть от этой конкретной реализации, Вместо этого вы всегда можете создать свою собственную логику потока глобального состояния, используя что-то вроде Stephen Cleary AsyncLocal (или .NET 4.6 AsyncLocal<T>).

Или, чтобы довести это до крайности, вы также можете полностью обрезать ContinueAwait и создать пользовательский awaiter, например. например ContinueOnScope. Это позволило бы точно контролировать, какой поток/контекст продолжить и какое состояние будет протекать.

Ответ 2

Однако до сих пор остается вопрос о том, следует ли использовать ConfigureAwait (false) на верхнем уровне.

Эмпирическое правило для ConfigureAwait(false) - использовать его всякий раз, когда остальной части вашего метода не нужен контекст.

В ASP.NET "контекст" на самом деле не определен в любом месте. Он включает в себя такие вещи, как HttpContext.Current, пользовательский принцип и пользовательская культура.

Итак, вопрос действительно сводится к: "Требует ли Controller.Json контекст ASP.NET?" Конечно, возможно, что Json не заботится об этом контексте (поскольку он может писать текущий ответ от своих собственных членов контроллера), но OTOH делает "форматирование", что может потребовать возобновления культуры пользователя.

Я не знаю, требует ли Json контекст, но он не документирован так или иначе, и в целом я предполагаю, что любые вызовы в код ASP.NET могут зависеть от контекста. Поэтому я бы не использовал ConfigureAwait(false) на верхнем уровне в моем коде контроллера, просто чтобы быть в безопасности.