调用StackExchange.Redis时,我陷入了僵局。
我不知道到底是怎么回事,这非常令人沮丧,我将感谢您提供任何有助于解决或解决此问题的建议。
如果您也有这个问题,并且不想阅读所有这些内容; 我建议您尝试设置
PreserveAsyncOrder
为false
。
ConnectionMultiplexer connection = ...; connection.PreserveAsyncOrder = false;
这样做可能会解决此Q&A即将发生的僵局,并且还可以提高性能。
我们的设置
- 该代码既可以作为控制台应用程序运行,也可以作为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
(像我们这样,异步同步)导致死锁。但是我们的代码在没有同步上下文的情况下运行,因此不适用于这里,对吧? -
是的,我们不应该那样做。但是我们这样做了,我们将不得不继续这样做一段时间。许多代码需要迁移到异步世界中。
同样,我们没有同步上下文,因此这不应引起死锁,对吗?
设置
ConfigureAwait(false)
之前await
没有任何影响。 异步命令和Task.WhenAny在StackExchange.Redis中等待后发生超时异常
这是线程劫持问题。目前情况如何?这可能是问题所在吗?
-
从马克的答案:
...混合等待和等待不是一个好主意。除了死锁,这是“异步同步”-一种反模式。
但是他也说:
SE.Redis内部绕过同步上下文(对于库代码而言是正常的),因此它不应具有死锁
因此,据我了解,StackExchange.Redis应该与我们是否使用sync-over-async反模式无关。只是不建议这样做,因为它可能是其他代码中死锁的原因。
但是,据我所知,在这种情况下,死锁确实在StackExchange.Redis内部。如果我错了,请纠正我。
调试结果
我发现僵局似乎有其源ProcessAsyncCompletionQueue
上的124线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);
在某些情况下,第162行上的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会读这篇文章并提供一些反馈意见:-)
无同步上下文=默认同步上下文
我在上面已经写到我们的代码不使用同步上下文。这仅部分正确:代码作为控制台应用程序或Azure工作者角色运行。在这些环境中SynchronizationContext.Current
是null
,这就是为什么我写道我们在没有同步上下文的情况下运行。
但是,在阅读了有关SynchronizationContext的全部内容之后,我了解到情况并非如此:
按照约定,如果线程的当前SynchronizationContext为null,则它隐式具有默认的SynchronizationContext。
但是,默认的同步上下文不应成为死锁的原因,因为基于UI的(WinForms,WPF)同步上下文可以-因为它并不暗示线程亲和性。
我认为发生了什么
消息完成后,将检查其完成源是否被视为同步安全。如果是,则完成操作将以内联方式执行,一切都很好。
如果不是,则是对新分配的线程池线程执行完成操作。这也只是正常的时候ConnectionMultiplexer.PreserveAsyncOrder
是false
。
但是,当ConnectionMultiplexer.PreserveAsyncOrder
is为true
(默认值)时,这些线程池线程将使用完成队列并通过确保在任何时候它们中最多一个是活动的异步工作线程来序列化其工作。
当线程成为活动的异步工作线程时,它将继续保持该状态,直到耗尽完成队列为止。
问题是完成操作不同步安全(从上面开始),它仍然在不能阻塞的线程上执行,因为这将阻止其他非同步安全消息被完成。
请注意,使用完成操作正在完成的其他消息 ,即使活动的异步工作程序线程被阻止同步安全将继续正常工作。
我建议的“修复”(上述)不会以这种方式引起死锁,但是会与保存异步完成顺序的概念混淆。
因此,也许在这里得出的结论是,与/混合时不安全await
Result
Wait()
PreserveAsyncOrder
是true
,无论是否在没有同步上下文的情况下运行, is?
(我想至少要等到我们可以使用.NET 4.6和新版本为止TaskCreationOptions.RunContinuationsAsynchronously
)