gpt4 book ai didi

c# - 访问 StackExchange.Redis 时出现死锁

转载 作者:行者123 更新时间:2023-12-02 05:26:20 34 4
gpt4 key购买 nike

我在拨打 StackExchange.Redis 时遇到了死锁情况.

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

In case you have this problem too and don't want to read all this; I suggest that you'll try setting PreserveAsyncOrder to false.

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

Doing so will probably resolve the kind of deadlock that this Q&A is about and could also improve performance.



我们的设置
  • 代码作为控制台应用程序或 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?

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

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

    设置 ConfigureAwait(false)在任何之前 await对此没有影响。
  • Timeout exception after async commands and Task.WhenAny awaits in StackExchange.Redis

    这就是线程劫持问题。这方面目前的情况如何?这可能是这里的问题吗?
  • StackExchange.Redis async call hangs

    从马克的回答:

    ...mixing Wait and await is not a good idea. In addition to deadlocks, this is "sync over async" - an anti-pattern.



    但他也说:

    SE.Redis bypasses sync-context internally (normal for library code), so it shouldn't have the deadlock



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

    然而,在这种情况下,据我所知,死锁确实发生在 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);

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

    然而,看完 It's All About the SynchronizationContext我了解到事实并非如此:

    By convention, if a thread’s current SynchronizationContext is null, then it implicitly has a default SynchronizationContext.



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

    我认为会发生什么

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

    如果不是,想法是在新分配的线程池线程上执行完成操作。当 ConnectionMultiplexer.PreserveAsyncOrder 时,这也很好用是 false .

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

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

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

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

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

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

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

    最佳答案

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

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

    关于c# - 访问 StackExchange.Redis 时出现死锁,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/30797716/

    34 4 0
    Copyright 2021 - 2024 cfsdn All Rights Reserved 蜀ICP备2022000587号
    广告合作:1813099741@qq.com 6ren.com