访问StackExchange.Redis

时间:2015-06-12 07:35:32

标签: c# asynchronous deadlock stackexchange.redis

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

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

  

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

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

这样做可能会解决此Q& A所涉及的死锁类型,并且还可以提高性能。

我们的设置

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

死锁

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

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

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

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

相关

  • StackExchange.Redis Deadlocking

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

  • How to safely mix sync and async code?

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

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

    在任何ConfigureAwait(false)之前设置await对此无效。

  • Timeout exception after async commands and Task.WhenAny awaits in StackExchange.Redis

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

  • StackExchange.Redis async call hangs

    来自Marc的回答:

      

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

    但他也说:

      

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

    因此,根据我的理解,StackExchange.Redis应该不知道我们是否正在使用 sync-over-async 反模式。它不推荐使用,因为它可能是其他代码中死锁的原因。

    但是,在这种情况下,据我所知,死锁实际上是在StackExchange.Redis中。如果我错了,请纠正我。

调试结果

我发现死锁似乎来源ProcessAsyncCompletionQueue line 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正在等待它认为是活动异步工作线程的线程,而它实际上是一个与此完全相反的线程。

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

怎么做?

我试图找出的主要两个问题是:

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

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

  3. 可能的解决办法?

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

    next.TryComplete(true);
    

    ...在line 162 in CompletionManager.cs上可能在某些情况下让当前线程(活动异步工作线程)徘徊并开始处理其他代码,可能导致死锁。 / p>

    在不知道细节的情况下,只考虑这个"事实",在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为null,则它隐式具有默认的SynchronizationContext。

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

    我认为发生了什么

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

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

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

    当一个线程成为活动的异步工作线程时,它将继续存在,直到它耗尽完成队列

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

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

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

    因此,在await Result Wait()时,可能会得出结论PreserveAsyncOrdertrue / {{1}}混合是不安全的,无论我们是否在没有同步上下文的情况下运行?

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

2 个答案:

答案 0 :(得分:20)

这些是我在这个死锁问题中找到的解决方法:

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

然后,只要在一个拥有&#34;拥有的线程上完成任务就可以允许内联任务。由自定义调度程序:

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

答案 1 :(得分:0)

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

详细说明了HTTP请求的限制。类似于旧的WCF问题,当您没有处理连接,然后所有WCF连接都将失败。

Max number of concurrent HttpWebRequests

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

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