访问StackExchange.Redis时出现死锁


73

调用StackExchange.Redis时,我陷入了僵局。

我不知道到底是怎么回事,这非常令人沮丧,我将感谢您提供任何有助于解决或解决此问题的建议。


如果您也有这个问题,并且不想阅读所有这些内容; 我建议您尝试设置PreserveAsyncOrderfalse

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

这样做可能会解决此Q&A即将发生的僵局,并且还可以提高性能。


我们的设置

  • 该代码既可以作为控制台应用程序运行,也可以作为Azure工作者角色运行。
  • 它使用HttpMessageHandler公开REST api,因此入口点是异步的。
  • 代码的某些部分具有线程相似性(由单个线程拥有,并且必须由单个线程运行)。
  • 代码的某些部分是仅异步的。
  • 我们正在做异步同步异步同步的反模式。(混合awaitWait()/ Result)。
  • 访问Redis时,我们仅使用异步方法。
  • 我们将.NET 4.5使用StackExchange.Redis 1.0.450。

僵局

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

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

我们还从低优先级的后台线程对Redis进行了调用,即使死锁发生后,这些调用仍继续起作用。

似乎只有在线程池线程上调用Redis时才会发生死锁。我不再认为这是由于这些调用是在线程池线程上进行的。而是,即使发生死锁情况,似乎任何没有继续执行或具有同步安全继续执行的异步Redis调用都将继续起作用。(请参阅下面我的看法

有关

  • StackExchange.Redis死锁

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

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

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

    同样,我们没有同步上下文,因此这不应引起死锁,对吗?

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

  • 异步命令和Task.WhenAny在StackExchange.Redis中等待后发生超时异常

    这是线程劫持问题。目前情况如何?这可能是问题所在吗?

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

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

该怎么办?

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

  1. 即使在没有同步上下文的情况下运行,混合awaitWait()/也Result可能成为死锁的原因吗?

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

但是,在阅读了有关SynchronizationContext的全部内容之后我了解到情况并非如此:

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

但是,默认的同步上下文不应成为死锁的原因,因为基于UI的(WinForms,WPF)同步上下文可以-因为它并不暗示线程亲和性。

我认为发生了什么

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

如果不是,则是对新分配的线程池线程执行完成操作。这也只是正常的时候ConnectionMultiplexer.PreserveAsyncOrderfalse

但是,当ConnectionMultiplexer.PreserveAsyncOrderis为true(默认值)时,这些线程池线程将使用完成队列并通过确保在任何时候它们中最多一个是活动的异步工作线程来序列化其工作。

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

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

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

我建议的“修复”(上述)不会以这种方式引起死锁,但是会与保存异步完成顺序的概念混淆

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

我想至少要等到我们可以使用.NET 4.6和新版本为止TaskCreationOptions.RunContinuationsAsynchronously


在这里很难形成意见,因为您没有显示任何实际调用SE.Redis的代码,也没有进行任何等待/等待-这是关键代码...您可以表明您正在调用它吗?
Marc Gravell

@MarcGravell:我可以向您展示任何代码,尽管不是全部。但是,问题在于我不知道哪个代码是这里有趣的部分。请查看我最近的编辑(最后),我认为问题是普遍的,并且由于活动的异步工作线程正在执行非同步安全完成操作,因此在阻塞时会导致死锁。
貂Wikström

2
虽然不是答案,但写得很好。
Nico 2015年

当调用async方法的同步上下文是它试图返回的同步上下文时,即使它来自后台线程,也会在asp.net应用程序中引起同步异步死锁。
eran otzap

我在某些特定情况下看到了相同的情况,在我的本地开发环境中可以重现。不知道是什么触发了此问题,但这是完全相同的死锁症状-qs表示已发送邮件,in表示已接收邮件,但挂起了。这是对SE Redis的完全同步调用,完全没有异步。设置PreserveAsyncOrder可以解决此问题,但这似乎有些不可思议。@MarcGravell有什么想法吗?
克里斯·海因斯

Answers:


23

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

解决方法#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...
}

实施好的任务计划程序可能是一项复杂的任务。但是,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);
}

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

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
注意:自版本2.0.495 PreserveAsyncOrder起已弃用。
tehmas

@tehmas在哪里新的标志就是内部的任何建议ConnectionMultiplexerPreserveAsyncOrder变得过时?还是在其他地方存在标志StackExchange.Redis
chy600

-1

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

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

并发HttpWebRequests的最大数量

因为我怀疑您是否确实在使用所有TCP端口,但是这是有关调试的辅助工具,但是有关如何查找您有多少个开放端口以及到达何处的好信息。

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


谢谢。但是,此问题不是由TCP端口或HTTP连接用尽引起的。
貂Wikström
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.