访问StackExchange.Redis时出现死锁

Mår*_*röm 72 c# deadlock asynchronous stackexchange.redis

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

我不确切知道发生了什么,这是非常令人沮丧的,我希望任何有助于解决或解决此问题的输入.


万一你也有这个问题,不想读这一切; 我建议你将尝试设置PreserveAsyncOrderfalse.

ConnectionMultiplexer connection = ...;
connection.PreserveAsyncOrder = false;
Run Code Online (Sandbox Code Playgroud)

这样做可能会解决此Q&A所涉及的僵局,并且还可以提高性能.


我们的设置

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

僵局

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

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

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

似乎只有在线程池线程上调用Redis时才会出现死锁.我不再认为这是因为这些调用是在线程池线程上进行的.相反,似乎任何异步Redis调用没有延续,或者同步安全延续,即使在发生死锁情况后也会继续工作.(见下面我认为发生的事情)

有关

  • StackExchange.Redis死锁

    混合引起的死锁awaitTask.Result(像我们一样同步异步).但是我们的代码在没有同步上下文的情况下运行,因此这里不适用,对吧?

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

    是的,我们不应该这样做.但我们这样做了,我们将不得不继续这样做一段时间.需要迁移到异步世界的大量代码.

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

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

  • 异步命令和Task.WhenAny等待StackExchange.Redis之后的超时异常

    这是线程劫持问题.目前的情况如何?这可能是问题吗?

  • StackExchange.Redis异步调用挂起

    来自Marc的回答:

    ...混合等待和等待不是一个好主意.除了死锁之外,这是"同步异步" - 一种反模式.

    但他也说:

    SE.Redis在内部绕过sync-context(库代码正常),所以它不应该有死锁

    因此,根据我的理解,StackExchange.Redis应该与我们是否使用同步异步反模式无关.它只是不推荐,因为它可能是其他代码中死锁的原因.

    但是,在这种情况下,据我所知,死锁实际上是在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);
}
Run Code Online (Sandbox Code Playgroud)

我发现在僵局期间; activeAsyncWorkerThread是我们正在等待Redis调用完成的线程之一.(我们的线程 =运行我们代码的线程池线程).所以上面的循环被认为是永远的.

不知道细节,这肯定是错的; StackExchange.Redis正在等待它认为是活动的异步工作线程的线程,而它实际上是一个与之完全相反的线程.

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

该怎么办?

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

  1. 即使在没有同步上下文的情况下运行,也可能混淆awaitWait()/ Result或导致死锁?

  2. 我们是否遇到StackExchange.Redis中的错误/限制?

可能的解决办法?

从我的调试结果来看,问题似乎是:

next.TryComplete(true);
Run Code Online (Sandbox Code Playgroud)

...在第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
    }
}
Run Code Online (Sandbox Code Playgroud)

我想我最大的希望是Marc Gravell会读到这个并提供一些反馈:-)

无同步上下文=默认同步上下文

我在上面写过,我们的代码不使用同步上下文.这只是部分正确:代码作为控制台应用程序或Azure辅助角色运行.在这些环境SynchronizationContext.Currentnull,这就是为什么我写道我们在没有同步上下文的情况下运行.

但是,在阅读了It的所有关于SynchronizationContext之后我才知道事实并非如此:

按照惯例,如果线程的当前SynchronizationContext为null,则它隐式具有默认的SynchronizationContext.

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

我认为发生了什么

消息完成后,将检查其完成源是否被认为是同步安全的.如果是,则完成操作是内联执行的,一切都很好.

如果不是,那么想法是在新分配的线程池线程上执行完成操作.这也是工作的时候就好了ConnectionMultiplexer.PreserveAsyncOrderfalse.

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

当一个线程成为活动的异步工作线程时,它将一直是那个,直到它耗尽了完成队列.

问题是完成操作不是同步安全的(从上面开始),它仍然在不能被阻止的线程上执行,因为这将阻止其他非同步安全消息的完成.

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

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

因此,这里可能得出的结论,无论是否在没有同步上下文的情况下运行,/ 何时混合都是不安全的awaitResultWait()PreserveAsyncOrdertrue

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

Mår*_*röm 21

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

解决方法#1

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

通过设置PreserveAsyncOrder为禁用该行为false.

ConnectionMultiplexer connection = ...;
connection.PreserveAsyncOrder = false;
Run Code Online (Sandbox Code Playgroud)

这样可以避免死锁,也可以提高性能.

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

您将放弃以与底层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...
}
Run Code Online (Sandbox Code Playgroud)

实现良好的任务调度程序可能是一项复杂的任务.但是,您可以使用ParallelExtensionExtras库(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);
}
Run Code Online (Sandbox Code Playgroud)

另一个想法是使用线程本地存储将调度程序附加到其所有线程.

private static ThreadLocal<TaskScheduler> __attachedScheduler 
                   = new ThreadLocal<TaskScheduler>();
Run Code Online (Sandbox Code Playgroud)

确保在线程开始运行时分配此字段并在完成时清除:

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;
    }
}
Run Code Online (Sandbox Code Playgroud)

然后,只要在自定义调度程序"拥有"的线程上完成任务,就可以允许内联任务:

public override bool TryExecuteTaskInline(Task task, bool taskWasPreviouslyQueued)
{
    // Allow inlining on our own threads.
    return __attachedScheduler.Value == this && this.TryExecuteTask(task);
}
Run Code Online (Sandbox Code Playgroud)

  • 注意:从 **Release 2.0.495** 开始,`PreserveAsyncOrder` 已被弃用。 (2认同)