【问题标题】:The cost of locking锁定成本
【发布时间】:2015-10-20 09:04:03
【问题描述】:

我有一个缓存(由 Web 应用程序使用),它在内部使用两个缓存 - 一个短期缓存,仅在请求中使用,一个长期缓存“永久”使用(跨请求) .

我有以下代码,请注意所有底层数据结构都是线程安全的。

public TCache Get(CacheDependency cachdeDependancy, Func<CacheDependency, TCache> cacheItemCreatorFunc)
{
    TCache cacheItem;
    if (shortTermCache.TryGetValue(cachdeDependancy.Id, out cacheItem))
    {
        return cacheItem;
    }

    DateTime cacheDependancyLastModified;
    if (longTermCache.TryGetValue(cachdeDependancy.Id, out cacheItem)
        && IsValid(cachdeDependancy, cacheItem, out cacheDependancyLastModified))
    {
        cacheItem.CacheTime = cacheDependancyLastModified;
        shortTermCache[cachdeDependancy.Id] = cacheItem;
        return cacheItem;
    }

    cacheItem = cacheItemCreatorFunc(cachdeDependancy);

    longTermCache.Add(cachdeDependancy.Id, cacheItem);
    shortTermCache[cachdeDependancy.Id] = cacheItem;
    return cacheItem;
}

显然,当运行并发(即多个 Web 请求)时,上面的代码仍然有可能(甚至可能)不一致。 但是我写了一些单元测试,我看到的是从来没有发生过“异常”。可能发生的情况是再次添加相同的项目,即使它已经存在等等。--> 我想你可以在查看代码时明白我的意思。

我仍然认为拥有一个始终正确且一致的解决方案会很好。

所以我使用简单的双重检查锁定机制重写了这段代码(也许这可能会更好,通过为另一个缓存添加另一个/第二个锁定?):

public TCache Get(CacheDependency cachdeDependancy, Func<CacheDependency, TCache> cacheItemCreatorFunc)
{
    TCache cacheItem;
    if (shortTermCache.TryGetValue(cachdeDependancy.Id, out cacheItem))
    {
        return cacheItem;
    }

    lock (_lockObj)
    {
        if (shortTermCache.TryGetValue(cachdeDependancy.Id, out cacheItem))
        {
            return cacheItem;
        }

        DateTime cacheDependancyLastModified;
        if (longTermCache.TryGetValue(cachdeDependancy.Id, out cacheItem)
            && IsValid(cachdeDependancy, cacheItem, out cacheDependancyLastModified))
        {
            cacheItem.CacheTime = cacheDependancyLastModified;
            shortTermCache[cachdeDependancy.Id] = cacheItem;
            return cacheItem;
        }

        cacheItem = cacheItemCreatorFunc(cachdeDependancy);

        longTermCache.Add(cachdeDependancy.Id, cacheItem);
        shortTermCache[cachdeDependancy.Id] = cacheItem;
        return cacheItem;
    }
}

我认为这段代码现在可以在多线程环境中正常工作。

但我不确定的是: 这会不会非常慢,因此也会破坏缓存的目的?忍受这个问题会更好吗,缓存有时会出现“不一致”的行为? 因为如果同时有1000个web请求,都得等到能进入lock zone。或者这根本不是一个真正的问题,因为 CPU 一次只有特定数量的内核(因此是“真正的”并行线程),而且这种性能损失总是很小的?

【问题讨论】:

  • 这很大程度上取决于 2 个想法:当前有多少线程访问您的代码,以及在 lock 语句中运行代码需要多长时间。只要它只访问内存(没有数据库,没有磁盘)并且你调用的方法很快,它应该不是一个真正的问题。 (如果您的第 1000 个请求需要等待一毫秒左右,这不会成为瓶颈)但同样,它取决于实际情况。
  • 您可以使用Amdahls law计算并行化的改进。
  • 只能同时创建一个缓存项,这似乎是一个杀手。

标签: c# multithreading locking


【解决方案1】:

如果你使用ConcurrentDictionary,你已经有办法做你想做的事——你可以简单地使用GetOrAdd方法:

shortTermCache[cacheDependency.Id] = 
  longTermCache.GetOrAdd(cacheDependency.Id, _ => cacheItemCreatorFunc(cachdeDependancy));

快速简单:)

您甚至可以将其扩展为包括短期缓存检查:

return
  shortTermCache.GetOrAdd
  (
    cacheDependency.Id,
    _ =>
    {
      return longTermCache
             .GetOrAdd(cacheDependency.Id, __ => cacheItemCreatorFunc(cacheDependency));
    }
  );

虽然对于每个请求的缓存使用 ConcurrentDictionary 有点不必要 - 但它并不一定是线程安全的。

至于您的原始代码,是的,它已损坏。您在测试期间看不到这一事实并不令人惊讶 - 多线程问题通常难以重现。这就是为什么您首先要正确编码的原因 - 这意味着您必须了解到底发生了什么,以及可能发生什么样的并发问题。在您的情况下,有两个共享引用:longTermCachecacheItem 本身。即使您正在使用的所有对象都是线程安全的,您也不能保证 您的 代码也是线程安全的 - 在您的情况下,可能存在对 @ 的争用987654328@(这有多线程安全?),或者有人可能同时添加了相同的缓存项。

这种中断的具体方式在很大程度上取决于实际的实现——例如,Add 可能会在具有相同 Id 的项目已经存在时抛出异常,也可能不存在。您的代码可能希望所有缓存项都是相同的引用,也可能不是。 cacheItemCreatorFunc 可能有可怕的副作用或运行起来很昂贵,也可能没有。

添加lock 的更新确实解决了这些问题。但是,它无法处理您在各处泄漏cacheItem 的方式,例如。除非cacheItem 也完全是线程安全的,否则您可能会遇到一些难以跟踪的错误。而且我们已经知道它也不是不可变的 - 至少,您正在更改缓存时间。

【讨论】:

  • 感谢您的回答。 Tbh 我已经只为 ShortTermCache 使用了字典 - 我只是想阻止讨论,关于在任何地方使用“线程安全数据结构”;)我真正感兴趣的(如果甚至可以发表声明)如何您认为整个“锁定”的事情会对性能产生影响。
  • @OschtärEi 这不是一般无法回答的问题。很可能会有零影响 - 或者它可能会使您的整个应用程序停止。为什么还要维护请求本地缓存?
  • 我是这么认为的......好吧,IsStillValid 方法需要一点时间才能完成 - 所以我只想在请求中这样做一次,然后使用短期缓存进行进一步访问。
猜你喜欢
  • 1970-01-01
  • 2011-04-08
  • 2016-11-07
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2012-02-03
  • 1970-01-01
  • 2012-05-01
相关资源
最近更新 更多