【问题标题】:StackExchange.Redis DeadlockingStackExchange.Redis 死锁
【发布时间】:2015-01-29 20:32:20
【问题描述】:

我在我的 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

我几乎可以肯定这是某种僵局。我什至认为这可能与this question 有关。但我不知道该怎么办。

ConnectionMultiplexer 在 Nancy 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 来并行化请求。

【问题讨论】:

  • 还在总结。
  • 您是否有机会使用事务或批处理?有一种非常简单的方法可以让自己陷入僵局(这很容易解决)
  • @MarcGravell Nope...仍在处理示例的其余部分...有很多移动的部分...
  • @ChristopherPfohl 基本上说复用器没有以任何方式处于活动状态;一切都已完成并返回给调用者(用于同步)或 TPL(用于异步)。我想知道这是否简单:没有完全正确地使用 TPL。我必须查看调用代码才能发表评论。
  • 顺便说一句;如果您已经在异步领域; dapper 也有一个异步 API;p

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


【解决方案1】:

我们开始吧:

return Cache.UseCache1().Result

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

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

注意:StackExchange.Redis 使用同步上下文;它显式断开与同步上下文的连接以避免成为死锁的原因(这是正常的,建议用于库)。关键是您的代码没有

选项:

  • 不要混用async.Result/.Wait(),或者
  • 让您的所有await 调用(或至少在.UseCache1() 下的调用)显式调用.ConfigureAwait(false) - 但是请注意,这意味着完成不会在调用上下文中发生!

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

这是一个很常见的问题; I did pretty much the same thing.

【讨论】:

  • 你摇滚。我也很高兴你做了同样的事情。非常感谢你解释得这么好。
  • 我的堆栈基本上是:NancyFx + Marc Gravell...所以从源头获得帮助总是很高兴;)
猜你喜欢
  • 2015-08-28
  • 1970-01-01
  • 1970-01-01
  • 2017-12-27
  • 2014-06-08
  • 2013-11-30
  • 2013-12-25
  • 2017-03-02
  • 2023-03-14
相关资源
最近更新 更多