访问 StackExchange.Redis 时死锁

我在调用 StackExchange Redis时遇到了僵局。

我不知道到底发生了什么,这很令人沮丧,我希望任何投入,可以帮助解决或解决这个问题。


如果你也有这个问题,并且不想阅读所有这些; 我建议您尝试将 PreserveAsyncOrder设置为 false

ConnectionMultiplexer connection = ...;
connection.PreserveAsyncOrder = false;

这样做可能会解决此问答所涉及的那种死锁,并且还可以提高性能。


我们的圈套

  • 代码以控制台应用或 Azure 工作者角色的形式运行。
  • 它使用 HttpMessageHandler公开 REST api,因此入口点是异步的。
  • 代码的某些部分具有线程关联(由单个线程拥有,并且必须由单个线程运行)。
  • 代码的某些部分是仅异步的。
  • 我们正在做的 < em > 同步-超越-异步< em > 异步-超同步 反模式。(混合 awaitWait()/Result)。
  • 我们只在访问 Redis 时使用异步方法。
  • 我们在.NET 4.5中使用 StackExchange.Redis1.0.450。

僵局

当应用程序/服务启动时,它正常运行一段时间,然后突然(几乎)所有传入的请求都停止工作,它们永远不会产生响应。所有这些请求都处于死锁状态,等待对 Redis 的调用完成。

有趣的是,一旦死锁发生,对 Redis 的任何调用都将挂起,但只有当这些调用来自传入的 API 请求时才会挂起,而这些请求是在线程池上运行的。

我们还从低优先级的后台线程调用 Redis,这些调用甚至在死锁发生后仍继续运行。

似乎只有在线程池线程上调用 Redis 时才会发生死锁。我不再认为这是因为这些调用是在线程池线程上进行的。相反,似乎任何异步 Redis 调用 如果没有继续,或者使用 < em > sync safe 继续,将继续工作,甚至在发生死锁情况之后。(见下文 我认为会发生的事)

相关资料

  • StackExchange. Redis 死锁

    由于混合了 awaitTask.Result(同步-异步,就像我们做的那样)而导致的死锁。但是我们的代码在没有同步上下文的情况下运行,所以这里不适用,对吗?

  • 如何安全地混合同步和异步代码?

    是的,我们不应该这么做。但我们需要,而且我们必须继续这样做一段时间。需要将大量代码迁移到异步环境中。

    同样,我们没有同步上下文,所以这不会导致死锁,对吗?

    在任何 await之前设置 ConfigureAwait(false)对此没有影响。

  • 异步命令和 Task 之后的超时异常

    这就是线程劫持的问题。目前的情况如何? 这会是问题所在吗?

  • Redis 异步调用挂起

    马克的回答是:

    把等待和等待混为一谈可不是个好主意。除了死锁之外,这是“异步同步”——一种反模式。

    但他也表示:

    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 正在等待一个它认为是 主动异步工作线程的线程,而实际上它是一个完全相反的线程。

我不知道这是否是由于 线程劫持问题(我不完全理解) ?

怎么办?

我想弄明白的主要两个问题是:

  1. 在没有同步上下文的情况下,混合使用 awaitWait()/Result会导致死锁吗?

  2. 我们是否在 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.Currentnull,这就是为什么我写了我们正在运行 没有同步上下文。

然而,在阅读了 It’s All About the SynchronizationContext 之后,我发现事实并非如此:

按照约定,如果线程的当前 SynchronizationContext 为 null,则它隐式具有默认的 SynchronizationContext。

默认的同步上下文不应该是死锁的原因,但是,基于 UI (WinForms,WPF)的同步上下文可以-因为它不意味着线程亲和力。

我认为会发生的事

当消息完成时,将检查其完成源是否被认为是 同步安全。如果是这样,则以内联方式执行完成操作,一切正常。

如果不是,那么就在新分配的线程池线程上执行完成操作。当 ConnectionMultiplexer.PreserveAsyncOrderfalse时,这也可以很好地工作。

但是,当 ConnectionMultiplexer.PreserveAsyncOrdertrue(默认值)时,那些线程池线程将使用 完成队列序列化它们的工作,并确保在任何时候它们中最多只有一个是 主动异步工作线程

当一个线程变成 主动异步工作线程时,它将继续保持这个状态,直到它排空了 完成队列

问题是完成操作是 不同步安全(从上面) ,但它仍然在一个线程上执行,因为 不能被阻挡会阻止其他 非同步保险箱消息被完成。

请注意,其他正在使用完成操作完成的消息,即使 主动异步工作线程被阻塞,是安全的仍将继续正常工作。

我建议的“修复”(上面)不会以这种方式造成僵局,但会与 保持异步完成顺序的概念混淆。

所以这里的结论可能是 当 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 之前)

11219 次浏览

基于上面的详细信息,我猜测了很多,但是我不知道你的源代码。这听起来像你可能正在打击一些内部的,可配置的,限制在。网。您不应该碰到这些对象,所以我的猜测是,您没有处理对象,因为它们是在线程之间浮动的,这不允许您使用 using 语句来清楚地处理它们的对象生命周期。

这详细说明了 HTTP 请求的限制。与旧的 WCF 问题类似,当您没有释放连接时,所有的 WCF 连接都会失败。

最大并发 HttpWebRequest 数

这更多的是一个调试帮助,因为我怀疑你真的使用所有的 TCP 端口,但好的信息,如何找到多少打开的端口,你有到哪里。

Https://msdn.microsoft.com/en-us/library/aa560610(v=bts.20).aspx

以下是我找到的解决这个死锁问题的方法:

解决方案 # 1

默认情况下,StackExchange。Redis 将确保按照接收结果消息的相同顺序完成命令。这可能会导致本问题中所描述的僵局。

通过将 PreserveAsyncOrder设置为 false来禁用该行为。

ConnectionMultiplexer connection = ...;
connection.PreserveAsyncOrder = false;

这将避免死锁,也可以 提高绩效

我鼓励遇到死锁问题的任何人尝试这种变通方法,因为它是如此干净和简单。

您将失去异步延续的调用顺序与底层 Redis 操作完成顺序相同的保证。然而,我真的不明白为什么这是你可以依赖的东西。


解决方案 # 2

死锁发生在 StackExchange.Redis 中的 主动异步工作线程完成命令和内联执行完成任务时。

通过使用定制的 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...
}

实现一个好的任务调度器可能是一个复杂的任务。但是,在 并行扩展程序库(NuGet 软件包)中有一些现有的实现,您可以使用它们或从中获得灵感。

如果您的任务调度程序将使用自己的线程(而不是线程池中的线程) ,那么允许内联可能是一个好主意,除非当前线程来自线程池。这将工作,因为在 StackExchange 中的 主动异步工作线程。Redis 总是线程池线程。

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);
}