锁定成本

The cost of locking

我有一个缓存(由 Web 应用程序使用),它在内部使用两个缓存 - 一个短期缓存,仅在请求中使用,另一个长期缓存使用 "permanently" (跨请求)。

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

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;
}

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

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

所以我使用简单的双重检查锁机制重写了这段代码(也许这会更好,通过为其他缓存添加 another/second 锁?):

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;
    }
}

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

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

如果你使用 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 本身。即使您正在使用的所有对象都是线程安全的,您也无法保证 您的 代码也是线程安全的 - 在您的情况下,可能有关于 cacheItem 的争用(这有多线程安全?),或者有人可能同时添加了相同的缓存项。

具体如何中断在很大程度上取决于实际的实现 - 例如,如果具有相同 ID 的项目已经存在,Add 可能会抛出异常,也可能不存在。您的代码可能期望所有缓存项都是相同的引用,也可能不是。 cacheItemCreatorFunc 可能会产生可怕的副作用或对 运行 来说代价高昂,也可能不会。

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