C#-锁定成本

我有一个缓存(由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请求,则所有请求都必须等待,直到它们可以进入锁定区域.还是这根本不是一个问题,因为CPU一次仅具有特定数量的内核(因此只有“真正的”并行线程),并且这种性能下降总是很小的?

解决方法:

如果使用ConcurrentDictionary,则已经可以执行所需的操作-您只需使用GetOrAdd方法即可:

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

快捷方便 :)

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

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

尽管对于每个请求的缓存都不需要使用ConcurrentDictionary-它实际上并不是必须是线程安全的.

至于您的原始代码,是的,它已损坏.您在测试中没有看到的事实并不令人惊讶-多线程问题通常很难重现.这就是为什么您想要编码正确,首先也是最重要的原因-这意味着您必须了解到底发生了什么,以及可能发生哪种并发问题.在您的情况下,有两个共享引用:longTermCache和cacheItem本身.即使您使用的所有对象都是线程安全的,也无法保证代码也是线程安全的–在您的情况下,可能会在cacheItem上引起争执(线程安全性如何? ?),否则有人可能同时添加了相同的缓存项.

中断的确切程度在很大程度上取决于实际的实现-例如,如果已经存在具有相同ID的项,则Add可能会引发异常,否则可能不会.您的代码可能希望所有缓存项都使用相同的引用,也可能不是. cacheItemCreatorFunc可能具有可怕的副作用,或者运行起来很昂贵,或者可能没有.

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

上一篇:java – ReentrantReadWriteLock多个读取线程


下一篇:c# – 检查列表Count属性时出现多线程问题