Ответ 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
. Это позволило бы точно контролировать, какой поток/контекст продолжить и какое состояние будет протекать.