我在调用 StackExchange Redis时遇到了僵局。
我不知道到底发生了什么,这很令人沮丧,我希望任何投入,可以帮助解决或解决这个问题。
如果你也有这个问题,并且不想阅读所有这些; 我建议您尝试将
PreserveAsyncOrder
设置为false
。ConnectionMultiplexer connection = ...; connection.PreserveAsyncOrder = false;
这样做可能会解决此问答所涉及的那种死锁,并且还可以提高性能。
await
和 Wait()
/Result
)。当应用程序/服务启动时,它正常运行一段时间,然后突然(几乎)所有传入的请求都停止工作,它们永远不会产生响应。所有这些请求都处于死锁状态,等待对 Redis 的调用完成。
有趣的是,一旦死锁发生,对 Redis 的任何调用都将挂起,但只有当这些调用来自传入的 API 请求时才会挂起,而这些请求是在线程池上运行的。
我们还从低优先级的后台线程调用 Redis,这些调用甚至在死锁发生后仍继续运行。
似乎只有在线程池线程上调用 Redis 时才会发生死锁。我不再认为这是因为这些调用是在线程池线程上进行的。相反,似乎任何异步 Redis 调用 如果没有继续,或者使用 < em > sync safe 继续,将继续工作,甚至在发生死锁情况之后。(见下文 我认为会发生的事)
由于混合了 await
和 Task.Result
(同步-异步,就像我们做的那样)而导致的死锁。但是我们的代码在没有同步上下文的情况下运行,所以这里不适用,对吗?
是的,我们不应该这么做。但我们需要,而且我们必须继续这样做一段时间。需要将大量代码迁移到异步环境中。
同样,我们没有同步上下文,所以这不会导致死锁,对吗?
在任何 await
之前设置 ConfigureAwait(false)
对此没有影响。
这就是线程劫持的问题。目前的情况如何? 这会是问题所在吗?
马克的回答是:
把等待和等待混为一谈可不是个好主意。除了死锁之外,这是“异步同步”——一种反模式。
但他也表示:
Redis 在内部绕过同步上下文(对于库代码来说是正常的) ,所以它不应该有死锁
据我所知 StackExchange。Redis 应该不知道我们是否正在使用 同步超异步反模式。只是不建议这样做,因为它可能会导致 其他代码中的死锁。
然而,在这种情况下,据我所知,死锁实际上是在 StackExchange 内部。雷迪斯。如果我说错了,请纠正我。
我发现死锁的源头似乎在 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 调用完成。(我们的线 = 运行 我们的准则的线程池线程)。因此,上面的循环被认为是永远继续下去。
在不知道细节的情况下,这肯定感觉不对;。Redis 正在等待一个它认为是 主动异步工作线程的线程,而实际上它是一个完全相反的线程。
我不知道这是否是由于 线程劫持问题(我不完全理解) ?
我想弄明白的主要两个问题是:
在没有同步上下文的情况下,混合使用 await
和 Wait()
/Result
会导致死锁吗?
我们是否在 StackExchange.Redis 中遇到了 bug/限制?
从我的调试结果来看,问题似乎是:
next.TryComplete(true);
... ... 在某些情况下,可能会让当前线程(即 主动异步工作线程)脱离并开始处理其他代码,这可能会导致死锁。
在不知道细节和只考虑这个“事实”的情况下,那么在 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 工作者角色的形式运行。在这些环境中,SynchronizationContext.Current
是 null
,这就是为什么我写了我们正在运行 没有同步上下文。
然而,在阅读了 It’s All About the SynchronizationContext 之后,我发现事实并非如此:
按照约定,如果线程的当前 SynchronizationContext 为 null,则它隐式具有默认的 SynchronizationContext。
默认的同步上下文不应该是死锁的原因,但是,基于 UI (WinForms,WPF)的同步上下文可以-因为它不意味着线程亲和力。
当消息完成时,将检查其完成源是否被认为是 同步安全。如果是这样,则以内联方式执行完成操作,一切正常。
如果不是,那么就在新分配的线程池线程上执行完成操作。当 ConnectionMultiplexer.PreserveAsyncOrder
为 false
时,这也可以很好地工作。
但是,当 ConnectionMultiplexer.PreserveAsyncOrder
是 true
(默认值)时,那些线程池线程将使用 完成队列序列化它们的工作,并确保在任何时候它们中最多只有一个是 主动异步工作线程。
当一个线程变成 主动异步工作线程时,它将继续保持这个状态,直到它排空了 完成队列。
问题是完成操作是 不同步安全(从上面) ,但它仍然在一个线程上执行,因为 不能被阻挡会阻止其他 非同步保险箱消息被完成。
请注意,其他正在使用完成操作完成的消息,即使 主动异步工作线程被阻塞,是安全的仍将继续正常工作。
我建议的“修复”(上面)不会以这种方式造成僵局,但会与 保持异步完成顺序的概念混淆。
所以这里的结论可能是 当 ABC3为 ABC4时,将 ABC0与 ABC1/Wait()
混合是不安全的,无论我们是否在没有同步上下文的情况下运行?
(至少在我们可以使用.NET 4.6和新的 < a href = “ https://msdn.microsoft.com/en-us/library/system.threading.tasks.taskcreationoptions (v = vs. 110) .aspx”rel = “ noReferrer”> TaskCreationOptions.RunContinuationsAsynchronously
之前)