Я столкнулся с ситуацией тупика при вызове StackExchange.Redis.
Я не знаю точно, что происходит, что очень расстраивает, и я был бы признателен за любые материалы, которые могут помочь решить проблему или решить эту проблему.
Если у вас тоже есть эта проблема и вы не хотите читать все это;Я предлагаю вам попробовать установить
PreserveAsyncOrder
наfalse
.ConnectionMultiplexer connection = ...; connection.PreserveAsyncOrder = false;
Выполнение этого, вероятно, решит ту тупиковую ситуацию, в которой этот Q & A, и может также повысить производительность.
Наша настройка
- Код запускается как консольное приложение или роль Azure Worker.
- Он предоставляет REST api, используя HttpMessageHandler, чтобы точка входа была асинхронной.
- Некоторые части кода имеют сходство потоков (принадлежат и должны управляться одним потоком).
- Некоторые части кода асинхронны.
- Мы выполняем sync-over-async и async-over -sync анти-шаблоны. (смешивание
await
иWait()
/Result
). - Мы используем методы async при доступе к Redis.
- Мы используем StackExchange.Redis 1.0.450 для .NET 4.5.
Тупик
Когда приложение/служба запущено, он работает нормально какое-то время, а все внезапные (почти) все входящие запросы прекращают функционировать, они никогда не выдают ответ. Все эти запросы зашли в тупик, ожидая завершения вызова Redis.
Интересно, что после возникновения тупика любой вызов Redis будет зависать, но только если эти вызовы сделаны из входящего запроса API, которые запускаются в пуле потоков.
Мы также вызываем Redis из фоновых потоков с низким приоритетом, и эти вызовы продолжают функционировать даже после возникновения взаимоблокировки.
Кажется, что тупик произойдет только при вызове Redis в потоке пула потоков. Я больше не думаю, что это связано с тем, что эти вызовы выполняются в потоке пула потоков. Скорее, кажется, что любой асинхронный Redis вызывает без продолжения или с сохранением синхронизации, будет продолжать работать даже после возникновения ситуации взаимоблокировки. (Смотрите, что я думаю, происходит ниже)
Связанные
-
StackExchange.Redis Deadlocking
Тупик вызван смешением
await
иTask.Result
(sync-over-async, как и мы). Но наш код запускается без контекста синхронизации, поэтому здесь не применяется, правильно? -
Как безопасно смешивать синхронизацию и асинхронный код?
Да, мы не должны этого делать. Но мы это делаем, и нам придется продолжать делать это некоторое время. Множество кода, который необходимо перенести в мир асинхронных сетей.
Опять же, у нас нет контекста синхронизации, поэтому это не должно вызывать взаимоблокировки, правильно?
Настройка
ConfigureAwait(false)
до того, как любойawait
не повлияет на это. -
Исключение ожидания после асинхронных команд и Task.WhenAny ждет в StackExchange.Redis
Это проблема захвата потока. Какова нынешняя ситуация на этом? Это может быть проблема здесь?
-
Анективный вызов StackExchange.Redis висит
Из ответа Marc:
... смешивание Ожидание и ожидание - не очень хорошая идея. В дополнение к взаимоблокировкам это "синхронизация через async" - анти-шаблон.
Но он также говорит:
SE.Redis обходит синхронный контекст внутри (нормальный для кода библиотеки), поэтому он не должен иметь тупик
Итак, из моего понимания StackExchange.Redis должно быть агностично, используем ли мы анти-шаблон sync-over-async. Это просто не рекомендуется, поскольку это может быть причиной взаимоблокировок в другом коде.
В этом случае, однако, насколько я могу судить, тупик действительно находится внутри StackExchange.Redis. Пожалуйста, поправьте меня, если я ошибаюсь.
Результаты отладки
Я обнаружил, что тупик, похоже, имеет источник в 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
иWait()
/Result
быть причиной взаимоблокировок даже при работе без контекста синхронизации? -
Мы сталкиваемся с ошибкой/ограничением в 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 Worker. В этих средах SynchronizationContext.Current
- null
, поэтому я написал, что мы работаем без контекста синхронизации.
Однако после чтения It All About SynchronizationContext Я узнал, что на самом деле это не так:
По соглашению, если текущий поток SynchronizationContext имеет значение NULL, то он неявно имеет стандартный SynchronizationContext.
Контекст синхронизации по умолчанию не должен быть причиной взаимоблокировок, поскольку контекст синхронизации на основе UI (WinForms, WPF) может - потому что он не подразумевает сближение потоков.
То, что я думаю, происходит
Когда сообщение завершено, его источник завершения проверяется на предмет того, считается ли он безопасным. Если это так, действие завершения выполняется в строке, и все в порядке.
Если это не так, идея состоит в том, чтобы выполнить действие завершения для недавно выделенного потока пула потоков. Это тоже прекрасно работает, если ConnectionMultiplexer.PreserveAsyncOrder
false
.
Однако, когда ConnectionMultiplexer.PreserveAsyncOrder
является true
(значение по умолчанию), то эти потоки пула потоков будут сериализовывать свою работу с использованием очереди завершения и гарантируя, что максимум один из них является активным рабочим потоком асинхронного пользователя в любое время.
Когда поток становится активным асинхронным рабочим потоком, он будет продолжаться до тех пор, пока он не исчерпал очередь завершения.
Проблема заключается в том, что действие завершения не является безопасным с помощью синхронизации (сверху), но оно выполняется в потоке, который не должен быть заблокирован, поскольку это предотвратит выполнение других несинхронизируемых безопасных сообщений.
Обратите внимание, что другие сообщения, которые выполняются с помощью действия завершения, которое является безопасным с синхронизацией, будут продолжать работать нормально, даже если активный рабочий рабочий поток отключен.
Мое предложенное "исправление" (выше) не вызовет тупиковой ситуации таким образом, однако оно будет противоречить понятию сохранения порядка завершения асинхронизации.
Итак, может быть, сделать вывод, что небезопасно смешивать await
с Result
/Wait()
, когда PreserveAsyncOrder
есть true
, независимо от того, являемся ли мы работает без контекста синхронизации?
(По крайней мере, пока мы не сможем использовать .NET 4.6 и новый TaskCreationOptions.RunContinuationsAsynchronously
, предположим)