Я захожу в тупиковую ситуацию при вызове StackExchange.Redis.
Я не знаю точно, что происходит, что очень расстраивает, и я был бы признателен за любой вклад, который мог бы помочь решить или обойти эту проблему.
Если у вас тоже есть эта проблема и вы не хотите все это читать; я предлагаю вам попробовать установить
PreserveAsyncOrder
наfalse
.ConnectionMultiplexer connection = ...; connection.PreserveAsyncOrder = false;
Это, вероятно, разрешит тупик, о котором идет речь в данном разделе вопросов и ответов, а также может улучшить производительность.
Наша установка
- Код запускается либо как консольное приложение, либо как рабочая роль Azure.
- Он предоставляет REST api с использованием HttpMessageHandler, поэтому точка входа является асинхронной.
- Некоторые части кода имеют сходство с потоками (принадлежат и должны выполняться одним потоком).
- Некоторые части кода являются асинхронными.
- Мы выполняем синхронизацию через асинхронность < / em> и async- чрезмерная синхронизация антипаттернов. (смешивание
await
и _5 _ / _ 6_). - Мы используем только асинхронные методы при доступе к Redis.
- Мы используем StackExchange.Redis 1.0.450 для .NET 4.5.
Тупик
Когда приложение / служба запускается, оно какое-то время работает нормально, затем внезапно (почти) все входящие запросы перестают работать, они никогда не дают ответа. Все эти запросы находятся в тупике, ожидая завершения вызова Redis.
Интересно, что при возникновении взаимоблокировки любой вызов Redis будет зависать, но только если эти вызовы сделаны из входящего запроса API, который выполняется в пуле потоков.
Мы также выполняем вызовы Redis из фоновых потоков с низким приоритетом, и эти вызовы продолжают работать даже после возникновения взаимоблокировки.
Кажется, что взаимоблокировка возникнет только при вызове Redis в потоке пула потоков. Я больше не думаю, что это связано с тем, что эти вызовы выполняются на поток пула потоков. Скорее, похоже, что любой асинхронный вызов Redis без продолжения или с продолжением sync safe будет продолжать работать даже после возникновения тупиковой ситуации. (См. Что, по моему мнению, происходит ниже)
Связанный
Взаимоблокировка StackExchange.Redis
Тупик, вызванный смешиванием
await
иTask.Result
(асинхронная синхронизация, как мы). Но наш код запускается без контекста синхронизации, так что здесь это не применимо, верно?Как безопасно смешивать синхронизирующий и асинхронный код?
Да, мы не должны этого делать. Но мы это делаем, и нам придется продолжать это делать еще какое-то время. Большой объем кода, который необходимо перенести в асинхронный мир.
Опять же, у нас нет контекста синхронизации, так что это не должно вызывать взаимоблокировок, верно?
Установка
ConfigureAwait(false)
перед любымawait
на это не влияет.Исключение тайм-аута после асинхронных команд и Task.WhenAny ожидает в StackExchange.Redis
Это проблема перехвата потока. Какая сейчас ситуация по этому поводу? Может ли это быть проблемой?
- # P17 # # P18 #
# P19 #
# P20 ## P21 #
# P22 # # P23 #
Результаты отладки
Я обнаружил, что источник тупика, похоже, находится в ProcessAsyncCompletionQueue
на строка 124 из CompletionManager.cs
.
Фрагмент этого кода:
while (Interlocked.CompareExchange(ref activeAsyncWorkerThread, currentThread, 0) != 0)
{
// if we don't win the lock, check whether there is still work; if there is we
// need to retry to prevent a nasty race condition
lock(asyncCompletionQueue)
{
if (asyncCompletionQueue.Count == 0) return; // another thread drained it; can exit
}
Thread.Sleep(1);
}
Я обнаружил это во время тупика; activeAsyncWorkerThread
- это один из наших потоков, который ожидает завершения вызова Redis. (наш поток = поток пула потоков, выполняющий наш код). Таким образом, цикл выше считается продолжающимся вечно.
Не зная подробностей, это кажется неправильным; StackExchange.Redis ожидает потока, который, по его мнению, является активным асинхронным рабочим потоком, в то время как на самом деле это поток, совершенно противоположный этому.
Интересно, связано ли это с проблемой перехвата потока (которую я не совсем понимаю)?
Что делать?
Два основных вопроса, которые я пытаюсь понять:
Может ли смешивание
await
и _16 _ / _ 17_ быть причиной взаимоблокировок даже при работе без контекста синхронизации?Мы сталкиваемся с ошибкой / ограничением в StackExchange.Redis?
Возможное исправление?
Судя по результатам моей отладки, проблема в том, что:
next.TryComplete(true);
... в строке 162 дюйма CompletionManager.cs
может при некоторых обстоятельствах позволить текущему потоку (который является активным асинхронным рабочим потоком) уйти и начать обработку другого кода, что может вызвать взаимоблокировку.
Не зная подробностей и просто думая об этом «факте», было бы логично временно освободить активный асинхронный рабочий поток во время TryComplete
вызова.
Думаю, что-то вроде этого могло сработать:
// release the "active thread lock" while invoking the completion action
Interlocked.CompareExchange(ref activeAsyncWorkerThread, 0, currentThread);
try
{
next.TryComplete(true);
Interlocked.Increment(ref completedAsync);
}
finally
{
// try to re-take the "active thread lock" again
if (Interlocked.CompareExchange(ref activeAsyncWorkerThread, currentThread, 0) != 0)
{
break; // someone else took over
}
}
Думаю, я больше всего надеюсь на то, что Марк Гравелл прочитает это и поделится своим мнением :-)
Нет контекста синхронизации = Контекст синхронизации по умолчанию
Выше я писал, что в нашем коде не используется контекст синхронизации. Это верно лишь частично: код запускается либо как консольное приложение, либо как рабочая роль Azure. В этих средах _22 _ равно null
, поэтому я написал, что мы работаем без контекста синхронизации.
Однако после прочтения Все дело в контексте синхронизации я узнал, что это не совсем так:
По соглашению, если текущий SynchronizationContext потока имеет значение null, тогда он неявно имеет SynchronizationContext по умолчанию.
Однако контекст синхронизации по умолчанию не должен быть причиной взаимоблокировок, в отличие от контекста синхронизации на основе пользовательского интерфейса (WinForms, WPF), поскольку он не подразумевает сходство потоков.
Что я думаю происходит
Когда сообщение завершено, его источник завершения проверяется на предмет того, считается ли оно безопасным для синхронизации. Если это так, действие завершения выполняется встроенно, и все в порядке.
Если это не так, идея состоит в том, чтобы выполнить действие завершения для недавно выделенного потока пула потоков. Это тоже отлично работает, когда ConnectionMultiplexer.PreserveAsyncOrder
равно false
.
Однако, когда ConnectionMultiplexer.PreserveAsyncOrder
равно true
(значение по умолчанию), эти потоки пула потоков будут сериализовать свою работу, используя очередь завершения и гарантируя, что не более одного из них является активным асинхронным рабочим потоком. в любое время.
Когда поток становится активным асинхронным рабочим потоком, он будет оставаться таковым до тех пор, пока не опустошит очередь завершения.
Проблема в том, что действие завершения небезопасно для синхронизации (см. Выше), тем не менее оно выполняется в потоке, который не должен блокироваться, поскольку это предотвратит другие небезопасные для синхронизации сообщения от завершения.
Обратите внимание, что другие сообщения, которые завершаются с действием завершения, которое безопасно для синхронизации, будут продолжать работать нормально, даже если активный асинхронный рабочий поток заблокирован.
Предлагаемое мной «исправление» (см. Выше) не приведет к возникновению тупиковой ситуации таким образом, однако оно нарушит идею сохранения порядка асинхронного завершения.
Так что, возможно, здесь следует сделать вывод, что небезопасно смешивать await
с _29 _ / _ 30_, когда PreserveAsyncOrder
равно true
, независимо от того, работаем ли мы без контекста синхронизации?
(По крайней мере, пока мы не сможем использовать .NET 4.6 и новый _ 33_, я полагаю)
PreserveAsyncOrder
больше не поддерживается (устарело), интересно, это было исправлено в основной библиотеке? - person Matt Roberts   schedule 08.04.2020