StackExchange.Redis死锁

Cri*_*ole 7 task-parallel-library async-await stackexchange.redis

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

看看我的并行堆栈,看起来我有四个线程:

  1. 调用Result我的一个使用SE.R的任务的线程.(在堆栈上有很多Nancy东西,然后调用我的库使用SE.R,并调用Result.堆栈顶部是Monitor.Wait).
  2. 一个产生两个其他线程的线程.我认为这是由SE.R管理的.从堆栈开始Native to Managed Transition,ThreadHelper.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

我几乎可以肯定这是某种僵局.我甚至认为这可能与这个问题有关.但我不知道该怎么做.

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

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

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

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

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

Mar*_*ell 13

开始了:

return Cache.UseCache1().Result
Run Code Online (Sandbox Code Playgroud)

繁荣; 僵局.但与StackExchange.Redis无关.

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

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

选项:

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

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

这是一个非常普遍的问题; 我做了几乎相同的事情.