锁定

时间:2015-10-20 09:04:03

标签: c# multithreading locking

我有一个缓存(由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请求)时,上述代码仍然可能(甚至可能)不一致。 但是我写了一些单元测试,我看到的是从来没有发生过“异常”。可能发生的是,即使已经存在相同的项目,也会再次添加相同的项目 - &gt;我想你在看代码时可以看到我的意思。

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

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

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请求,则必须等到它们才能进入锁定区域。或者这根本不是一个问题,因为CPU只有一个特定数量的内核(因而是“真正的”并行线程),这种性能损失总是很小?

1 个答案:

答案 0 :(得分:4)

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

这究竟是如何中断在很大程度上取决于实际的实现 - 例如,如果具有相同Id的项目已经存在,或者它可能不存在,cacheItem可能会抛出异常。您的代码可能希望所有缓存项都是相同的引用,或者它可能不是。 Add可能会产生可怕的副作用或者运行起来很昂贵,或者可能没有。

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