访问 StackExchange.Redis 时出现死锁

Posted

技术标签:

【中文标题】访问 StackExchange.Redis 时出现死锁【英文标题】:Deadlock when accessing StackExchange.Redis 【发布时间】:2015-08-28 03:32:39 【问题描述】:

我在调用StackExchange.Redis 时遇到了死锁。

我不知道到底发生了什么,这非常令人沮丧,如果有任何意见可以帮助解决或解决此问题,我将不胜感激。


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

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

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


我们的设置

代码作为控制台应用程序或 Azure 辅助角色运行。 它使用HttpMessageHandler 公开一个 REST api,因此入口点是异步的。 代码的某些部分具有线程关联性(由单个线程拥有并且必须由单个线程运行)。 代码的某些部分是仅异步的。 我们正在执行sync-over-asyncasync-over-sync 反模式。 (混合awaitWait()/Result)。 我们只在访问 Redis 时使用异步方法。 我们将 StackExchange.Redis 1.0.450 用于 .NET 4.5。

死锁

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

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

我们还从低优先级后台线程调用 Redis,即使在死锁发生后这些调用也会继续运行。

似乎只有在线程池线程上调用 Redis 时才会发生死锁。我不再认为这是因为这些调用是在线程池线程上进行。相反,似乎任何异步 Redis 调用没有延续,或具有 同步安全延续,即使在死锁情况发生后, 仍将继续工作。 (请参阅下面的我认为会发生什么

相关

StackExchange.Redis Deadlocking

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

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

怎么办?

我想弄清楚的两个主要问题:

    即使在没有同步上下文的情况下运行,awaitWait()/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.Currentnull,这就是为什么我写道我们正在运行 同步上下文。

但是,在阅读It's All About the SynchronizationContext 之后,我了解到情况并非如此:

按照惯例,如果一个线程的当前 SynchronizationContext 为空,那么它隐含了一个默认的 SynchronizationContext。

默认同步上下文不应该是死锁的原因,因为基于 UI(WinForms,WPF)的同步上下文可能 - 因为它不暗示线程关联。

我认为会发生什么

当一条消息完成时,检查它的完成源是否被认为是同步安全。如果是,则内联执行完成操作,一切正常。

如果不是,想法是在一个新分配的线程池线程上执行完成动作。当ConnectionMultiplexer.PreserveAsyncOrderfalse 时,这也可以正常工作。

但是,当ConnectionMultiplexer.PreserveAsyncOrdertrue(默认值)时,这些线程池线程将使用完成队列 序列化它们的工作,并确保其中最多一个是活动的异步工作线程随时。

当一个线程成为活动的异步工作线程时,它将继续保持这种状态,直到它耗尽完成队列

问题是完成动作是不同步安全(从上面),它仍然在一个不能被阻塞的线程上执行,因为这会阻止其他非同步安全消息无法完成。

请注意,通过 同步安全 的完成操作完成的其他消息将继续正常工作,即使 活动的异步工作线程 被阻塞。

我建议的“修复”(上图)不会以这种方式导致死锁,但它会与保留异步完成顺序的概念混淆。

所以也许这里的结论是PreserveAsyncOrdertrue 时,将awaitResult/Wait() 混合是不安全的,无论我们是否是在没有同步上下文的情况下运行?

至少在我们可以使用 .NET 4.6 和新的TaskCreationOptions.RunContinuationsAsynchronously 之前,我想

【问题讨论】:

在这里很难形成意见,因为您没有显示任何实际调用 SE.Redis 的代码或进行任何等待/等待 - 这是关键代码......可以你表明你在叫它? @MarcGravell:我可以向您展示任何代码,尽管不是全部。但是,问题是我不知道哪个代码是这里有趣的部分。请查看我最近的编辑(最后),我认为问题是一般性的,并且由于 活动异步工作线程 正在执行 非同步安全 完成操作,这将阻塞时导致死锁。 虽然不是一个写得很好的问题的答案。 当调用 async 方法的同步上下文是它尝试返回的上下文时,即使它来自后台线程,也会在 asp.net 应用程序中导致同步异步死锁。 我在某些特定情况下看到了同样的场景,可以在我的本地开发环境中重现。不知道是什么触发了这个,但它是完全相同的死锁症状——qs 说东西已发送,in 说东西已接收,但它挂起。这是对 SE Redis 的完全同步调用,根本没有异步。设置 PreserveAsyncOrder 可以解决这个问题,但这似乎有点神奇。 @MarcGravell 对此有何想法? 【参考方案1】:

这些是我发现的解决这个死锁问题的解决方法:

解决方法 #1

默认情况下,StackExchange.Redis 将确保命令的完成顺序与接收结果消息的顺序相同。如本问题所述,这可能会导致死锁。

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

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

这样可以避免死锁,也可以improve performance。

我鼓励遇到死锁问题的任何人尝试这种解决方法,因为它非常干净和简单。

您将无法保证异步延续的调用顺序与底层 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...

实现一个好的任务调度器可能是一项复杂的任务。但是,ParallelExtensionExtras library (NuGet package) 中的现有实现可供您使用或从中汲取灵感。

如果您的任务调度程序将使用它自己的线程(而不是来自线程池),那么允许内联可能是一个好主意,除非当前线程来自线程池。这将起作用,因为 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);

另一个想法是使用thread-local storage 将您的调度程序附加到它的所有线程。

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

【讨论】:

注意:从 版本 2.0.495 开始,PreserveAsyncOrder 已被弃用。 @tehmas 关于ConnectionMultiplexer 内的新标志在PreserveAsyncOrder 被弃用后的位置有何建议?或者如果StackExchange.Redis 内的其他地方有标志?【参考方案2】:

我根据上面的详细信息猜测了很多,并且不知道您拥有的源代码。听起来您可能在 .Net 中遇到了一些内部的、可配置的限制。你不应该打那些,所以我猜你没有处理对象,因为它们在线程之间浮动,这不允许你使用 using 语句来干净地处理它们的对象生命周期。

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

Max number of concurrent HttpWebRequests

这更像是一个调试帮助,因为我怀疑你是否真的使用了所有的 TCP 端口,但是关于如何找到你有多少开放端口以及到哪里的有用信息。

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

【讨论】:

谢谢。但这个问题并不是因为 TCP 端口或 HTTP 连接用完而引起的。

以上是关于访问 StackExchange.Redis 时出现死锁的主要内容,如果未能解决你的问题,请参考以下文章

使用 StackExchange.Redis 封装属于自己的 RedisHelper

StackExchange.Redis Timeout performing 超时问题

StackExchange.Redis Timeout performing 超时问题

StackExchange.Redis .net core Timeout performing 超时问题

StackExchange.Redis实现Redis发布订阅

StackExchange.Redis 基本使用 (转)