Взаимоблокировка при доступе к StackExchange.Redis
я столкнулся с тупиковой ситуацией при вызове StackExchange.Редис.
я не знаю точно, что происходит, что очень расстраивает, и я был бы признателен за любой вклад, который может помочь решить или обойти эту проблему.
в случае, если у вас есть эта проблема, и не хочу читать все это;
Я предлагаю вам попробовать установитьPreserveAsyncOrderдоfalse.
ConnectionMultiplexer connection = ...;
connection.PreserveAsyncOrder = false;
делаешь так вероятно, разрешит вид тупика, о котором идет речь в этом Q&A, а также может улучшить производительность.
наши установки
- код выполняется либо как консольное приложение, либо как рабочая роль Azure.
- он предоставляет REST api с помощью HttpMessageHandler таким образом, точка входа асинхронна.
- некоторые части кода имеют сходство с потоком (принадлежит и должен выполняться одним нить.)
- некоторые части кода являются только асинхронными.
- мы занимаемся синхронизации-за асинхронной и асинхронность-за синхронизации анти-паттерны. (смешивание
awaitиWait()/Result). - мы используем только асинхронные методы при доступе к Redis.
- мы используем StackExchange.Redis 1.0.450 для .NET 4.5.
тупик
когда приложение/сервис запущенный он работает нормально некоторое время, а затем внезапно (почти) все входящие запросы перестают функционировать, они никогда не дают ответа. Все эти запросы находятся в тупике, ожидая завершения вызова Redis.
интересно, что после возникновения взаимоблокировки любой вызов Redis будет зависать, но только если эти вызовы выполняются из входящего запроса API, который выполняется в пуле потоков.
мы также делаем вызовы Redis из низкоприоритетных фоновых потоков, и эти вызовы продолжают функционировать даже после возникновения взаимоблокировки.
похоже, что взаимоблокировка будет происходить только при вызове Redis в потоке пула потоков. я больше не думаю, что это связано с тем, что эти вызовы выполняются в потоке пула потоков. Скорее, это похоже на любой асинхронный вызов Redis без продолжения, или sync safe продолжение будет продолжать работать даже после того, как ситуация тупиковая произошло. (См.что я думаю, происходит ниже)
по теме
StackExchange.Взаимоблокировки Рэдис
тупик, вызванный смешиванием
awaitиTask.Result(синхронизация через асинхронный, как у нас). Но наш код выполняется без контекста синхронизации, так что это не применяется здесь, верно?
как безопасно смешивать синхронизацию и асинхронный код?
Да, мы не должны этого делать. Но мы делаем, и мы будем нужно продолжать делать это в течение некоторого времени. Много кода, который необходимо перенести в асинхронный мир.
опять же, у нас нет контекста синхронизации, поэтому это не должно вызывать взаимоблокировки, верно?
задание
ConfigureAwait(false)перед любымawaitне влияет на это.
исключение таймаута после асинхронных команд и задач.WhenAny ждет в StackExchange.Редис
это проблема захвата потока. Что такое ток ситуация по этому поводу? Может ли это быть проблемой здесь?
StackExchange.Redis асинхронный вызов зависает
из ответа Марка:
...смешивание ждать и ждать не очень хорошая идея. В дополнение к взаимоблокировкам, это "синхронизации за асинхронность" - это анти-паттерн.
но он же говорит:
SE.Redis обходит sync-context внутренне (обычно для кода библиотеки), поэтому он не должен иметь тупик
Итак, из моего понимания StackExchange.Redis должен быть агностиком к тому, используем ли мы синхронизации-за асинхронной анти-паттерн. Это просто не рекомендуется, так как это может быть причиной тупиков в другое код.
в этом случае, однако, насколько я могу судить, тупик действительно находится внутри StackExchange.Редис. Пожалуйста, поправьте меня, если я ошибаюсь.
результаты отладки
у меня есть найдено, что тупик, кажется, имеет свой источник в ProcessAsyncCompletionQueue on строка 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.Редис?
можно исправить?
из моих результатов отладки кажется, что проблема в том, что:
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
}
}
я думаю, что моя лучшая надежда заключается в том, что Марк Гравелл прочитает это и предоставит некоторую обратную связь : -)
нет синхронизации context = контекст синхронизации по умолчанию
я уже писал выше, что наш код не использовать контекст синхронизации. Это верно лишь частично: код выполняется либо как консольное приложение, либо как рабочая роль Azure. В этих условиях SynchronizationContext.Current и null, вот почему я написал, что мы бежим без контекст синхронизации.
однако, после прочтения это все о SynchronizationContext я понял, что это не совсем так:
по соглашению, если текущий SynchronizationContext потока равен null, то он неявно имеет SynchronizationContext по умолчанию.
контекст синхронизации по умолчанию не должен быть причиной взаимоблокировок, хотя, поскольку контекст синхронизации на основе пользовательского интерфейса (WinForms, WPF) может-потому что он не подразумевает сходство потоков.
что я думаю бывает
когда сообщение завершено, его источник завершения проверяется, считается ли он sync safe. Если это так, то действие завершения выполняется inline и все в порядке.
если это не так, идея состоит в том, чтобы выполнить действие завершения на вновь выделенном потоке пула потоков. Это тоже работает просто отлично, когда ConnectionMultiplexer.PreserveAsyncOrder и false.
, когда
ConnectionMultiplexer.PreserveAsyncOrder и true (значение по умолчанию), то те пула потоков потоки будут сериализовать свою работу с помощью завершению очереди и гарантируя, что не более одного из них является активный асинхронный рабочий поток в любое время.когда поток становится активный асинхронный рабочий поток это будет продолжаться до тех пор, пока он не осушил завершению очереди.
проблема в том, что завершение действия не синхронизация сейф (сверху), еще выполняется на a нить, что не должно быть заблокировано как то помешает другим non sync safe сообщения от завершения.
обратите внимание, что другие сообщения, которые завершаются с действием завершения, которое is sync safe будет продолжать работать просто отлично, даже если активный асинхронный рабочий поток блокируется.
мое предлагаемое "исправление" (выше) не приведет к тупику таким образом, однако это будет мешать понятие сохранение порядка асинхронного завершения.
так что, может быть, вывод сделать вот что это не безопасно смешивать await С Result/Wait(), когда PreserveAsyncOrder и true, независимо от того, работаем ли мы без контекста синхронизации?
(по крайней мере, пока мы не сможем использовать .NET 4.6 и новый TaskCreationOptions.RunContinuationsAsynchronously, я полагаю,)
2 ответов:
вот обходные пути, которые я нашел для этой проблемы тупика:
Решение #1
по умолчанию StackExchange.Redis гарантирует, что команды будут выполнены в том же порядке, что и полученные сообщения результата. Это может привести к взаимоблокировке, как описано в этом вопросе.
отключить это поведение, установив
PreserveAsyncOrderдоfalse.ConnectionMultiplexer connection = ...; connection.PreserveAsyncOrder = false;это позволит избежать тупиков, а также может улучшить производительность.
Я призываю всех, кто сталкивается с тупиковыми проблемами, попробовать этот обходной путь, поскольку он настолько чист и прост.
вы потеряете гарантию того, что асинхронные продолжения вызываются в том же порядке, что и базовые операции Redis. Тем не менее, я действительно не понимаю, почему это то, на что вы могли бы положиться.
решение #2
тупик возникает, когда активный асинхронный рабочий нить в StackExchange.Redis завершает команду и когда задача завершения выполняется inline.
можно предотвратить выполнение задачи в строке с помощью пользовательского
TaskSchedulerи убедиться, чтоTryExecuteTaskInlineвозвращаетfalse.public class MyScheduler : TaskScheduler { public override bool TryExecuteTaskInline(Task task, bool taskWasPreviouslyQueued) { return false; // Never allow inlining. } // TODO: Rest of TaskScheduler implementation goes here... }реализация хорошего планировщика задач может быть сложной задачей. Однако существуют существующие реализации в библиотека ParallelExtensionExtras (NuGet пакет), что вы можете использовать или черпать вдохновение.
если ваш планировщик задач будет использовать свои собственные потоки (не из пула потоков), то было бы неплохо разрешить встраивание, если текущий поток не из пула потоков. Это будет работать, потому что активный асинхронный рабочий поток в StackExchange.Redis-это всегда поток пула потоков.
public override bool TryExecuteTaskInline(Task task, bool taskWasPreviouslyQueued) { // Don't allow inlining on a thread pool thread. return !Thread.CurrentThread.IsThreadPoolThread && this.TryExecuteTask(task); }другой идеей было бы прикрепить планировщик ко всем его потокам, используя локальное хранилище потока.
private static ThreadLocal<TaskScheduler> __attachedScheduler = new ThreadLocal<TaskScheduler>();убедитесь, что это поле назначается при запуске потока и очищается по мере его завершения:
private void ThreadProc() { // Attach scheduler to thread __attachedScheduler.Value = this; try { // TODO: Actual thread proc goes here... } finally { // Detach scheduler from thread __attachedScheduler.Value = null; } }тогда вы можете разрешить встраивание задач, пока это делается в потоке, который" принадлежит " пользовательскому планировщику:
public override bool TryExecuteTaskInline(Task task, bool taskWasPreviouslyQueued) { // Allow inlining on our own threads. return __attachedScheduler.Value == this && this.TryExecuteTask(task); }
Я предполагаю много на основе подробной информации выше и не зная исходный код у вас есть на месте. Похоже, что вы можете столкнуться с некоторыми внутренними и настраиваемыми ограничениями в .Net. вы не должны их поражать, поэтому я предполагаю, что вы не избавляетесь от объектов, поскольку они плавают между потоками, которые не позволят вам использовать оператор using для чистой обработки их жизни объектов.
Это детализирует ограничения на HTTP-запросы. Аналогично старая проблема WCF, когда вы не избавились от соединения, а затем все соединения WCF потерпят неудачу.
максимальное количество одновременных HttpWebRequests
Это больше помощь в отладке, так как я сомневаюсь, что вы действительно используете все TCP-порты, но хорошая информация о том, как найти, сколько открытых портов у вас есть и где.
https://msdn.microsoft.com/en-us/library/aa560610 (v=bts. 20). aspx
Comments