c# - 锁定成本

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

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

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

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

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 一次只有特定数量的内核(因此是“真正的”并行线程),而且这种性能损失总是很小的?

最佳答案

如果您使用 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 也是完全线程安全的,否则您可能会遇到一些难以跟踪的错误。我们已经知道它也不是不可变的 - 至少,您正在更改缓存时间。

关于c# - 锁定成本,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/33232384/

相关文章:

java - java中如何同步方法?

java - 为什么线程的恢复和挂起方法应该同步?

c++ - 序列点是否会阻止代码跨临界区边界重新排序?

.net - 这个类应该使用数据锁定进行多线程吗?

c# - 减少大对象堆中同一个对象的多个副本

c# - 单击行中的按钮时获取 ListView 项目

c# - 带有单点触控的 Facebook api

C# 将控制台应用程序转换为服务

sql-server - SQL Server 2005 : Key-Range Locks in Read Committed Transaction Isolation Level?

c++ - 在 Win32/MFC 中停止线程