StackExchange.Redis死锁

时间:2014-12-01 18:49:20

标签: task-parallel-library async-await stackexchange.redis

我在我的Nancy应用程序中使用StackExchange.Redis(SE.R)。只有一个全局ConnectionMultiplexer由Nancy的TinyIoC通过构造函数参数自动传递,并且每当我尝试使用GetDatabase和其中一个*Async方法时(同步)方法只在尝试了其中一个异步方法后才开始失败)我的应用程序死锁。

看看我的并行堆栈似乎有四个线程:

  1. 在我的一个使用SE.R的任务上调用Result的线程。 (堆栈中有很多Nancy东西,然后调用我的库使用SE.R,并调用Result。堆栈顶部是Monitor.Wait)。
  2. 产生两个其他线程的线程。我认为这是由SE.R管理的。以Native to Managed TransitionThreadHelper.ThreadStart开头,堆栈顶部为ThreadHelper.ThreadStart_Context
  3. 像这样被卡住的小堆栈:
    • Monitor.Wait
    • Monitor.Wait
    • SocketManager.WriteAllQueues
    • SocketManager.cctor.AnonymousMethod__16
  4. 另一个看起来像这样的小筹码:
    • Managed to Native Transition
    • SocketManager.ReadImpl
    • SocketManager.Read
    • SocketManager.cctor.AnonymousMethod__19
  5. 我几乎可以肯定这是某种僵局。我甚至认为它可能与this question有关。但我不知道该怎么做。

    使用以下代码在南希ConnectionMultiplexer中设置IRegistrations

    var configOpts =  new ConfigurationOptions {
      EndPoints = {
        RedisHost,
      },
      Password = RedisPass,
      AllowAdmin = false,
      ClientName = ApplicationName,
      ConnectTimeout = 10000,
      SyncTimeout = 5000,
    };
    var mux = ConnectionMultiplexer.Connect(configOpts);
    yield return new InstanceRegistration(typeof (ConnectionMultiplexer), mux);
    

    mux是在构造函数参数列表中请求它的所有代码共享的实例。

    我有一个名为SchemaCache的班级。它的一小部分(包括引发错误的代码)如下:

    public SchemaCache(ConnectionMultiplexer connectionMultiplexer) {
        ConnectionMultiplexer = connectionMultiplexer;
    }
    
    private ConnectionMultiplexer ConnectionMultiplexer { get; set; }
    
    private async Task<string[]> Cached(string key, bool forceFetch, Func<string[]> fetch) {
        var db = ConnectionMultiplexer.GetDatabase();
    
        return forceFetch || !await db.KeyExistsAsync(key)
            ? await CacheSetSet(db, key, await Task.Run(fetch))
            : await CacheGetSet(db, key);
    }
    
    private static async Task<string[]> CacheSetSet(IDatabaseAsync db, string key, string[] values) {
        await db.KeyDeleteAsync(key);
        await db.SetAddAsync(key, EmptyCacheSentinel);
    
        var keysSaved = values
            .Append(EmptyCacheSentinel)
            .Select(val => db.SetAddAsync(key, val))
            .ToArray()
            .Append(db.KeyExpireAsync(key, TimeSpan.FromDays(1)));
        await Task.WhenAll(keysSaved);
    
        return values;
    }
    
    private static async Task<string[]> CacheGetSet(IDatabaseAsync db, string key) {
        var results = await db.SetMembersAsync(key);
        return results.Select(rv => (string) rv).Without(EmptyCacheSentinel).ToArray();
    }
    
    // There are a bunch of these public methods:
    public async Task<IEnumerable<string>> UseCache1(bool forceFetch = false) {
        return await Cached("the_key_i_want", forceFetch, () => {
            using (var cnn = MakeConnectionToDatabase("server", "databaseName")) {
                // Uses Dapper:
                return cnn.Query<string>("--expensive sql query").ToArray();
            }
        });
    }
    

    我还有一个类在需要缓存中的一些信息的方法中使用它:

    public OtherClass(SchemaCache cache) {
        Cache = cache;
    }
    
    private SchemaCache Cache { get; set; }
    
    public Result GetResult(Parameter parameter) {
        return Cache.UseCache1().Result
            .Where(r => Cache.UseCache2(r).Result.Contains(parameter))
            .Select(r => CheckResult(r))
            .FirstOrDefault(x => x != null);
    }
    

    以上所有这些在LinqPad中都很好用,其中只有一个问题的实例。相反,它失败了TimeoutException(后来是关于没有可用连接的例外)。唯一的区别是我通过依赖注入得到了一个缓存实例,我很确定Nancy使用Tasks来并行化请求。

1 个答案:

答案 0 :(得分:13)

我们走了:

return Cache.UseCache1().Result

吊杆;僵局。但与StackExchange.Redis无关。

至少来自大多数同步上下文提供商。这是因为您的await都隐式请求同步上下文激活 - 这可能意味着&#34;在UI线程&#34; (winforms,WPF),或者#34;在当前指定的工作线程上#34; (WCF,ASP.NET,MVC等)。这里的问题是此线程永远不可用于处理这些项,因为.Result是一个同步和阻塞调用。因此,没有任何完成回调将被处理,因为唯一可以处理它们的线程正在等待它们完成,然后才能使它自己可用。

注意:StackExchange.Redis 使用sync-context;它明确地断开与sync-context的连接,以避免成为死锁的原因(这是正常的,建议用于库)。关键是您的代码

选项:

  • 不要混用async.Result / .Wait(),或
  • 让所有await次调用(或至少.UseCache1()下面的调用)明确调用.ConfigureAwait(false) - 但是,请注意,这意味着完成将不会出现在调用上下文中!

第一种选择是最简单的;如果你可以隔离一个不依赖于同步上下文的调用树,那么第二种方法是可行的。

这是一个非常普遍的问题; I did pretty much the same thing