访问StackExchange.Redis时发生死锁

调用StackExchange.Redis时遇到了死锁情况。

我不知道到底发生了什么,这是非常令人沮丧的,我将不胜感激任何有助于解决或解决此问题的意见。


如果你也有这个问题,不想阅读所有这些; 我build议您尝试将PreserveAsyncOrder设置为false

 ConnectionMultiplexer connection = ...; connection.PreserveAsyncOrder = false; 

这样做可能会解决这个Q&A的僵局,也可能会提高性能。


我们的设置

  • 代码作为控制台应用程序或Azure工作者angular色运行。
  • 它使用HttpMessageHandler公开REST API,因此入口点是asynchronous的。
  • 代码的某些部分具有线程关联(属于并且必须由单个线程运行)。
  • 代码的某些部分是仅asynchronous的。
  • 我们正在进行asynchronous同步asynchronous同步反模式。 (混合awaitWait() / Result )。
  • 访问Redis时,我们只使用asynchronous方法。
  • 我们使用.NET 4.5的StackExchange.Redis 1.0.450。

僵局

当应用程序/服务启动后,它会正常运行一段时间,然后突然(几乎)所有传入请求停止运行,它们从不产生响应。 所有这些请求都被locking,等待Redis的调用完成。

有趣的是,一旦发生死锁,任何对Redis的调用都将挂起,但前提是这些调用是通过在线程池中运行的传入API请求进行的。

我们还从低优先级后台线程调用Redis,并且这些调用即使在发生死锁后也能继续工作。

看起来,只有在线程池线程中调用Redis时才会发生死锁。 我不再认为这是由于这些调用是在线程池线程上进行的。 相反,任何asynchronousRedis调用没有延续,或者同步安全继续,即使在死锁情况发生后,它仍然可以继续工作。 (请参阅下面的内容)

有关

  • StackExchange.Redis死锁

    混合引起的死锁awaitTask.Result (同步asynchronous,就像我们一样)。 但是我们的代码运行时没有同步上下文,所以在这里不适用,对吧?

  • 如何安全地混合同步和asynchronous代码?

    是的,我们不应该这样做。 但是我们这样做了,我们将不得不继续这样做一段时间。 很多需要迁移到asynchronous世界的代码。

    同样,我们没有同步上下文,所以这不应该造成死锁,对吧?

    在任何await之前设置ConfigureAwait(false)对此没有影响。

  • async命令和Task.WhenAny在StackExchange.Redis中等待之后的超时exception

    这是线程劫持问题。 目前情况如何? 这可能是这里的问题吗?

  • StackExchange.Redisasynchronous调用挂起

    从Marc的回答:

    混合等待,等待不是一个好主意。 除了死锁,这是“asynchronous同步” – 反模式。

    但他也说:

    SE.Redis在内部绕过同步上下文(对于库代码是正常的),所以它不应该有死锁

    所以,从我的理解StackExchange.Redis应该是不知道我们是否使用同步asynchronous反模式。 这只是不推荐,因为它可能是其他代码中的死锁的原因。

    然而在这种情况下,据我所知,死锁确实在StackExchange.Redis内部。 如果我错了,请纠正我。

debugging结果

我发现这个死锁似乎在CompletionManager.cs第124行的 ProcessAsyncCompletionQueue有其源码。

该代码片段:

 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正在等待一个线程,它认为它是活动的asynchronous工作线程,而实际上它是一个与此相反的线程。

我不知道这是由于线程劫持问题 (我不完全理解)?

该怎么办?

我试图找出主要的两个问题:

  1. 即使在没有同步上下文的情况下运行,混合是否会awaitWait() / Result是死锁的原因?

  2. 我们是否遇到StackExchange.Redis中的错误/限制?

一个可能的修复?

从我的debugging结果看来,问题在于:

 next.TryComplete(true); 

CompletionManager.cs第162行可能在某些情况下让当前线程(即活动的asynchronous工作线程 )漂移并开始处理其他代码,可能导致死锁。

在不知道细节的情况下,仅仅考虑这个“事实”,那么在TryComplete调用期间临时释放活动的asynchronous工作线程似乎是合乎逻辑的。

我想这样的事情可以工作:

 // 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 } } 

我想我最大的希望就是Marc Gravell会读这个,并提供一些反馈:-)

没有同步上下文=默认的同步上下文

我上面写过,我们的代码不使用同步上下文 。 这只是部分正确:代码作为控制台应用程序或Azure工作者angular色运行。 在这些环境中, SynchronizationContext.Currentnull ,这就是为什么我写了没有同步上下文的情况下运行。

但是,在阅读了“关于SynchronizationContext的所有内容”之后我了解到情况并非如此:

按照惯例,如果一个线程的当前SynchronizationContext为null,那么它隐式地有一个默认的SynchronizationContext。

因为基于UI(WinForms,WPF)的同步上下文可能会导致默认的同步上下文不应该成为死锁的原因,因为它并不意味着线程关联。

我的想法发生了

消息完成后,检查完成源是否被认为是同步安全的 。 如果是这样,完成操作是内联执行,一切都很好。

如果不是这样,则想法是在新分配的线程池线程上执行完成操作。 当ConnectionMultiplexer.PreserveAsyncOrderfalse时,也可以正常工作。

但是,如果ConnectionMultiplexer.PreserveAsyncOrdertrue (默认值),那么这些线程池线程将使用完成队列来序列化其工作,并确保至多其中一个线程在任何时候都是活动的asynchronous工作线程

当一个线程成为活动的asynchronous工作线程时 ,它将继续保持这个状态,直到完成队列完成

问题是完成操作不是同步安全的 (从上面),仍然在一个不能被阻塞的线程上执行,因为这将阻止其他非同步安全消息被完成。

请注意,正在使用同步安全的完成操作完成的其他消息将继续正常工作,即使活动的asynchronous工作线程被阻止。

我build议的“修复”(上面)不会导致这样的死锁,然而它会混淆保留asynchronous完成顺序的概念。

所以也许这里得出的结论是, PreserveAsyncOrdertrue ,无论我们是否在没有同步上下文的情况下运行,将ResultWait()混合await都是不安全的

至less直到我们可以使用.NET 4.6和新的TaskCreationOptions.RunContinuationsAsynchronously ,我想

这些是我发现这个死锁问题的解决方法:

解决方法#1

默认情况下,StackExchange.Redis将确保命令按照接收结果消息的顺序完成。 这可能会导致此问题中所述的死锁。

通过将PreserveAsyncOrder设置为false来禁用该行为。

 ConnectionMultiplexer connection = ...; connection.PreserveAsyncOrder = false; 

这将避免死锁,也可以提高性能 。

我鼓励任何遇到问题的人尝试这种解决方法,因为它非常干净和简单。

您将失去保证asynchronous延续以与底层Redis操作完成相同的顺序被调用。 但是,我真的不明白为什么这是你会依赖的。


解决方法#2

当StackExchange.Redis中的活动asynchronous工作线程完成一个命令并完成内嵌任务时,发生死锁。

通过使用自定义的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中的活动asynchronous工作线程始终是一个线程池线程。

 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中触及了一些内部和可configuration的限制。 你不应该碰到这些,所以我的猜测是你不会处理对象,因为它们在线程之间浮动,不允许你使用using语句来干净地处理对象的生命周期。

这详细说明了HTTP请求的限制。 类似于旧的WCF问题,当你没有处理连接,然后所有的WCF连接都会失败。

并发HttpWebRequest的最大数目

这更多的是一个debugging的帮助,因为我怀疑你真的在使用所有的TCP端口,但是关于如何find你有多less个开放的端口和在哪里有很好的信息。

https://msdn.microsoft.com/en-us/library/aa560610(v=bts.20).aspx