当调用StackExchange.Redis时,我遇到了死锁的情况。
我不知道具体发生了什么,这让我非常沮丧,我希望能得到任何有助于解决或解决此问题的输入。
In case you have this problem too and don't want to read all this; I suggest that you'll try setting
PreserveAsyncOrder
tofalse
.
ConnectionMultiplexer connection = ...; connection.PreserveAsyncOrder = false;
Doing so will probably resolve the kind of deadlock that this Q&A is about and could also improve performance.
我们的设置
- 代码作为控制台应用程序或 Azure 工作角色运行。
- 它使用 HttpMessageHandler 暴露了一个 REST api,因此入口点是异步的。
- 代码的某些部分具有线程亲和性(由单个线程拥有和运行)。
- 代码的某些部分仅支持异步。
- 我们正在使用 同步 - 异步 和 异步 - 同步 反模式。(混合使用
await
和Wait()
/Result
)。 - 只有在访问 Redis 时我们才使用异步方法。
- 我们使用 .NET 4.5 的 StackExchange.Redis 1.0.450。
死锁
当应用程序/服务启动后,它会正常运行一段时间,然后突然(几乎)所有传入请求停止工作,它们从未产生响应。所有这些请求都因等待 Redis 的调用而死锁。
有趣的是,一旦死锁发生,任何对 Redis 的调用都会挂起,但仅在这些调用来自运行在线程池上的传入 API 请求时才会发生。
我们也从低优先级后台线程中调用 Redis,这些调用即使在死锁发生后也继续正常工作。
似乎只有在线程池线程上调用 Redis 时才会发生死锁。 我不再认为这是因为这些调用是在线程池线程上进行的。相反,似乎任何没有延续或具有同步安全延续的异步 Redis 调用都将继续工作,即使在死锁情况发生后。(请参阅下面的 我认为会发生什么)
相关内容
-
由于混合使用
await
和Task.Result
(同步-over-异步,就像我们所做的那样)而导致死锁。但是我们的代码在没有同步上下文的情况下运行,因此这里不适用,对吧? -
是的,我们不应该这样做。但我们确实这样做了,并且我们将不得不继续这样做一段时间。很多需要迁移到异步世界的代码。
同样,我们没有同步上下文,因此这不应该引起死锁,对吧?
在任何
await
之前设置ConfigureAwait(false)
对此没有影响。 StackExchange.Redis中异步命令和Task.WhenAny等待后超时异常
这是线程劫持问题。目前的情况如何?这可能是问题所在吗?
-
来自Marc的回答:
...混合使用Wait和await不是一个好主意。除了死锁之外,这是“同步-over-异步”——一种反模式。
但他也说:
SE.Redis在内部绕过同步上下文(对于库代码而言很正常),因此它不应该有死锁问题
因此,据我所知,StackExchange.Redis应该对我们是否使用同步-over-异步反模式是无关紧要的。只是不建议这样做,因为它可能是其他代码中死锁的原因。
然而,在这种情况下,据我所知,死锁实际上是在StackExchange.Redis内部发生的。如果我错了,请纠正我。
调试结果
我发现死锁似乎源于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 调用完成的线程之一。(our thread = 运行 our code 的线程池线程)。因此,上面的循环被认为会无限继续。
不知道细节情况下,这肯定感觉不对;StackExchange.Redis 正在等待一个它认为是 active async worker thread 的线程,而实际上它却相反。
我想知道这是否由于 thread hijacking problem(我并不完全理解)引起的?
该怎么办?
我试图弄清楚的主要两个问题:
即使在没有同步上下文的情况下运行,混合使用
await
和Wait()
/Result
是否会导致死锁?我们是否遇到了 StackExchange.Redis 中的错误/限制?
一个可能的修复方法?
根据我的调试结果,似乎问题是:
next.TryComplete(true);
...在CompletionManager.cs的第162行中,某些情况下可能会让当前线程(即活动异步工作线程)离开并开始处理其他代码,从而可能导致死锁。
不了解细节,只考虑这个“事实”,那么在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
}
}
我想最好的希望是Marc Gravell能够阅读此文并提供反馈 :-)
没有同步上下文=默认同步上下文
我之前写过,我们的代码不使用同步上下文。这只是部分正确的:代码作为控制台应用程序或Azure Worker Role运行。在这些环境中,SynchronizationContext.Current
是null
,这就是为什么我说我们正在没有同步上下文的情况下运行。
然而,在阅读It's All About the SynchronizationContext之后,我了解到这并不是真的:
按照惯例,如果线程的当前同步上下文为null,则它隐式具有默认同步上下文。
默认的同步上下文不应该是死锁的原因,因为基于UI的(WinForms、WPF)同步上下文可能会死锁——因为它不意味着线程亲和性。
我认为发生了什么
当消息完成时,它的完成源会被检查是否被认为是同步安全的。如果是,则执行完成操作,并且一切都很好。
如果不是,那么想法是在新分配的线程池线程上执行完成操作。当ConnectionMultiplexer.PreserveAsyncOrder
为false
时,这也能正常工作。
然而,当ConnectionMultiplexer.PreserveAsyncOrder
为true
(默认值)时,那些线程池线程将使用完成队列序列化其工作,并确保最多只有一个线程是活动异步工作线程。
当线程成为活动异步工作线程时,它将继续保持状态,直到排空完成队列。
问题在于完成操作是不同步安全的(来自上面),但它在一个不能被阻塞的线程上执行,因为这将防止其他不同步安全的消息被完成。
请注意,其他使用同步安全的完成操作被完成的消息将继续正常工作,即使活动异步工作线程被阻塞。
我的建议“修复”(如上所述)不会以这种方式导致死锁,但是会干扰保留异步完成顺序的概念。
因此,也许可以得出结论:即使我们在没有同步上下文的情况下运行,当PreserveAsyncOrder
为true
时,混合使用await
和Result
/Wait()
是不安全的?
(至少直到我们能够使用.NET 4.6及新的TaskCreationOptions.RunContinuationsAsynchronously
,我想)