【发布时间】:2015-08-28 03:32:39
【问题描述】:
我在调用StackExchange.Redis 时遇到了死锁。
我不知道到底发生了什么,这非常令人沮丧,如果有任何意见可以帮助解决或解决此问题,我将不胜感激。
如果你也有这个问题,不想阅读所有这些; 我建议您尝试将
PreserveAsyncOrder设置为false。ConnectionMultiplexer connection = ...; connection.PreserveAsyncOrder = false;这样做可能会解决此问答所涉及的那种死锁,并且还可以提高性能。
我们的设置
- 代码作为控制台应用程序或 Azure 辅助角色运行。
- 它使用HttpMessageHandler 公开一个 REST api,因此入口点是异步的。
- 代码的某些部分具有线程关联性(由单个线程拥有并且必须由单个线程运行)。
- 代码的某些部分是仅异步的。
- 我们正在执行sync-over-async 和async-over-sync 反模式。 (混合
await和Wait()/Result)。 - 我们只在访问 Redis 时使用异步方法。
- 我们将 StackExchange.Redis 1.0.450 用于 .NET 4.5。
死锁
当应用程序/服务启动时,它会正常运行一段时间,然后突然(几乎)所有传入的请求都停止运行,它们永远不会产生响应。所有这些请求都处于死锁状态,等待对 Redis 的调用完成。
有趣的是,一旦发生死锁,任何对 Redis 的调用都会挂起,但前提是这些调用来自传入的 API 请求,这些请求在线程池上运行。
我们还从低优先级后台线程调用 Redis,即使在死锁发生后这些调用也会继续运行。
似乎只有在线程池线程上调用 Redis 时才会发生死锁。我不再认为这是因为这些调用是在线程池线程上进行。相反,似乎任何异步 Redis 调用没有延续,或具有 同步安全延续,即使在死锁情况发生后, 仍将继续工作。 (请参阅下面的我认为会发生什么)
相关
-
StackExchange.Redis Deadlocking
await和Task.Result混合导致的死锁(异步同步,就像我们做的那样)。但是我们的代码是在没有同步上下文的情况下运行的,所以这里不适用,对吧? -
How to safely mix sync and async code?
是的,我们不应该这样做。但我们这样做了,而且我们将不得不继续这样做一段时间。大量代码需要迁移到异步世界中。
同样,我们没有同步上下文,所以这不会导致死锁,对吧?
在任何
await之前设置ConfigureAwait(false)对此没有影响。 -
Timeout exception after async commands and Task.WhenAny awaits in StackExchange.Redis
这是线程劫持问题。目前这方面的情况如何?这可能是这里的问题吗?
-
StackExchange.Redis async call hangs
来自马克的回答:
...混合等待和等待不是一个好主意。除了死锁,这是“同步优于异步”——一种反模式。
但他也说:
SE.Redis 在内部绕过同步上下文(库代码正常),所以它不应该有死锁
所以,根据我的理解,StackExchange.Redis 应该不知道我们是否使用了 sync-over-async 反模式。只是不建议这样做,因为它可能会导致其他代码中的死锁。
然而,据我所知,在这种情况下,死锁确实在 StackExchange.Redis 内部。如果我错了,请纠正我。
调试结果
我发现死锁似乎在ProcessAsyncCompletionQueueline 124 of 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);
...on line 162 in 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
}
}
我想我最大的希望是Marc Gravell 会阅读此内容并提供一些反馈:-)
无同步上下文 = 默认同步上下文
我在上面写过我们的代码没有使用synchronization context。这只是部分正确:代码作为控制台应用程序或 Azure 辅助角色运行。在这些环境中,SynchronizationContext.Current 是 null,这就是为什么我写道我们正在运行 同步上下文。
但是,在阅读It's All About the SynchronizationContext 之后,我了解到情况并非如此:
按照惯例,如果一个线程的当前 SynchronizationContext 为空,那么它隐含了一个默认的 SynchronizationContext。
默认同步上下文不应该是死锁的原因,因为基于 UI(WinForms,WPF)的同步上下文可能 - 因为它不暗示线程关联。
我认为会发生什么
当一条消息完成时,检查它的完成源是否被认为是同步安全。如果是,则内联执行完成操作,一切正常。
如果不是,想法是在一个新分配的线程池线程上执行完成动作。当ConnectionMultiplexer.PreserveAsyncOrder 是false 时,这也可以正常工作。
但是,当ConnectionMultiplexer.PreserveAsyncOrder 为true(默认值)时,这些线程池线程将使用完成队列 序列化它们的工作,并确保其中最多一个是活动的异步工作线程随时。
当一个线程成为活动的异步工作线程时,它将继续保持这种状态,直到它耗尽完成队列。
问题是完成动作是不同步安全(从上面),它仍然在一个不能被阻塞的线程上执行,因为这会阻止其他非同步安全消息无法完成。
请注意,通过 同步安全 的完成操作完成的其他消息将继续正常工作,即使 活动的异步工作线程 被阻塞。
我建议的“修复”(上图)不会以这种方式导致死锁,但它会与保留异步完成顺序的概念混淆。
所以也许这里的结论是当PreserveAsyncOrder 是true 时,将await 与Result/Wait() 混合是不安全的,无论我们是否是在没有同步上下文的情况下运行?
(至少在我们可以使用 .NET 4.6 和新的TaskCreationOptions.RunContinuationsAsynchronously 之前,我想)
【问题讨论】:
-
在这里很难形成意见,因为您没有显示任何实际调用 SE.Redis 的代码或进行任何等待/等待 - 这是关键代码......可以你表明你在叫它?
-
@MarcGravell:我可以向您展示任何代码,尽管不是全部。但是,问题是我不知道哪个代码是这里有趣的部分。请查看我最近的编辑(最后),我认为问题是一般性的,并且由于 活动异步工作线程 正在执行 非同步安全 完成操作,这将阻塞时导致死锁。
-
虽然不是一个写得很好的问题的答案。
-
当调用 async 方法的同步上下文是它尝试返回的上下文时,即使它来自后台线程,也会在 asp.net 应用程序中导致同步异步死锁。
-
我在某些特定情况下看到了同样的场景,可以在我的本地开发环境中重现。不知道是什么触发了这个,但它是完全相同的死锁症状——qs 说东西已发送,in 说东西已接收,但它挂起。这是对 SE Redis 的完全同步调用,根本没有异步。设置 PreserveAsyncOrder 可以解决这个问题,但这似乎有点神奇。 @MarcGravell 对此有何想法?
标签: c# asynchronous deadlock stackexchange.redis