访问StackExchange.Redis时发生死锁
调用StackExchange.Redis时遇到了死锁情况。
我不知道到底发生了什么,这是非常令人沮丧的,我将不胜感激任何有助于解决或解决此问题的意见。
如果你也有这个问题,不想阅读所有这些; 我build议您尝试将
PreserveAsyncOrder
设置为false
。ConnectionMultiplexer connection = ...; connection.PreserveAsyncOrder = false;
这样做可能会解决这个Q&A的僵局,也可能会提高性能。
我们的设置
- 代码作为控制台应用程序或Azure工作者angular色运行。
- 它使用HttpMessageHandler公开REST API,因此入口点是asynchronous的。
- 代码的某些部分具有线程关联(属于并且必须由单个线程运行)。
- 代码的某些部分是仅asynchronous的。
- 我们正在进行asynchronous同步和asynchronous同步反模式。 (混合
await
和Wait()
/Result
)。 - 访问Redis时,我们只使用asynchronous方法。
- 我们使用.NET 4.5的StackExchange.Redis 1.0.450。
僵局
当应用程序/服务启动后,它会正常运行一段时间,然后突然(几乎)所有传入请求停止运行,它们从不产生响应。 所有这些请求都被locking,等待Redis的调用完成。
有趣的是,一旦发生死锁,任何对Redis的调用都将挂起,但前提是这些调用是通过在线程池中运行的传入API请求进行的。
我们还从低优先级后台线程调用Redis,并且这些调用即使在发生死锁后也能继续工作。
看起来,只有在线程池线程中调用Redis时才会发生死锁。 我不再认为这是由于这些调用是在线程池线程上进行的。 相反,任何asynchronousRedis调用没有延续,或者同步安全继续,即使在死锁情况发生后,它仍然可以继续工作。 (请参阅下面的内容)
有关
-
StackExchange.Redis死锁
混合引起的死锁
await
和Task.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工作线程,而实际上它是一个与此相反的线程。
我不知道这是由于线程劫持问题 (我不完全理解)?
该怎么办?
我试图找出主要的两个问题:
-
即使在没有同步上下文的情况下运行,混合是否会
await
,Wait()
/Result
是死锁的原因? -
我们是否遇到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.Current
为null
,这就是为什么我写了没有同步上下文的情况下运行。
但是,在阅读了“关于SynchronizationContext的所有内容”之后,我了解到情况并非如此:
按照惯例,如果一个线程的当前SynchronizationContext为null,那么它隐式地有一个默认的SynchronizationContext。
因为基于UI(WinForms,WPF)的同步上下文可能会导致默认的同步上下文不应该成为死锁的原因,因为它并不意味着线程关联。
我的想法发生了
消息完成后,检查完成源是否被认为是同步安全的 。 如果是这样,完成操作是内联执行,一切都很好。
如果不是这样,则想法是在新分配的线程池线程上执行完成操作。 当ConnectionMultiplexer.PreserveAsyncOrder
为false
时,也可以正常工作。
但是,如果ConnectionMultiplexer.PreserveAsyncOrder
为true
(默认值),那么这些线程池线程将使用完成队列来序列化其工作,并确保至多其中一个线程在任何时候都是活动的asynchronous工作线程 。
当一个线程成为活动的asynchronous工作线程时 ,它将继续保持这个状态,直到完成队列完成 。
问题是完成操作不是同步安全的 (从上面),仍然在一个不能被阻塞的线程上执行,因为这将阻止其他非同步安全消息被完成。
请注意,正在使用同步安全的完成操作完成的其他消息将继续正常工作,即使活动的asynchronous工作线程被阻止。
我build议的“修复”(上面)不会导致这样的死锁,然而它会混淆保留asynchronous完成顺序的概念。
所以也许这里得出的结论是, 当PreserveAsyncOrder
为true
,无论我们是否在没有同步上下文的情况下运行,将Result
和Wait()
混合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