此延迟加载缓存实现是否线程安全



我正在使用 3.5 .NET Framework 进行开发,我需要在多线程场景中使用缓存,对其项目具有延迟加载模式。在阅读了网络上的几篇文章后,我尝试编写自己的实现。

public class CacheItem
{
    public void ExpensiveLoad()
    {
        // some expensive code
    }
}
public class Cache
{
    static object SynchObj = new object();
    static Dictionary<string, CacheItem> Cache = new Dictionary<string, CacheItem>();
    static volatile List<string> CacheKeys = new List<string>();
    public CacheItem Get(string key)
    {
        List<string> keys = CacheKeys;
        if (!keys.Contains(key))
        {
            lock (SynchObj)
            {
                keys = CacheKeys;
                if (!keys.Contains(key))
                {
                    CacheItem item = new CacheItem();
                    item.ExpensiveLoad();
                    Cache.Add(key, item);
                    List<string> newKeys = new List<string>(CacheKeys);
                    newKeys.Add(key);
                    CacheKeys = newKeys;
                }
            }
        }
        return Cache[key];
    }
}

如您所见,Cache 对象既使用存储真实键值对的字典,也使用仅复制键的列表。当线程调用 Get 方法时,它会读取静态共享密钥列表(声明为易失性(并调用 Include 方法以查看密钥是否已存在,如果不存在,则在开始延迟加载之前使用双重检查的锁定模式。在加载结束时,将创建键列表的新实例并将其存储在静态变量中。

显然,我的情况是,重新创建整个键列表的成本与单个项目加载的成本几乎无关紧要。

我希望有人能告诉我它是否真的是线程安全的。当我说"线程安全"时,我的意思是每个读取器线程都可以避免损坏或脏读,并且每个写入器线程只加载一次丢失的项目。

这不是线程安全的,因为您在阅读字典时没有锁定。

存在一个线程可以读取的争用条件:

return Cache[key];

而另一个正在写:

_Cache.Add(key, item);

正如 MSDN Dictionary<TKey,TValue>文档所述:">

若要允许多个线程访问集合以进行读取和写入,必须实现自己的同步。

并且您的同步不包括阅读器。

真的需要使用线程安全的字典,这将极大地简化你的代码(你根本不需要列表(

我建议获取.NET 4 ConcurrentDictionary的源代码。

获得正确的线程安全是很困难的,其他一些回答者错误地指出您的实现是线程安全的这一事实就证明了这一点。 因此,我相信Microsoft在自制之前实现。

如果您不想使用线程安全字典,那么我建议您使用简单的东西,例如:

public CacheItem Get(string key)
{
    lock (SynchObj)
    {
        CacheItem item;
        if (!Cache.TryGetValue(key, out item))
        {
            item = new CacheItem();
            item.ExpensiveLoad();
            Cache.Add(key, item);
        }
        return item;
    }
}

您也可以尝试使用ReaderWriterLockSlim实现,尽管您可能不会获得显着的性能改进(Google用于ReaderWriterLockSlim性能(。

至于使用ConcurrentDictionary的实现,在大多数情况下,我会简单地使用类似的东西:

static ConcurrentDictionary<string, CacheItem> Cache = 
    new ConcurrentDictionary<string, CacheItem>(StringComparer.Ordinal);
...
CacheItem item = Cache.GetOrAdd(key, key => ExpensiveLoad(key));

这可能会导致每个键调用ExpensiveLoad多次,但我敢打赌,如果您分析您的应用程序,您会发现这种情况非常罕见,以至于不是问题。

如果你真的坚持确保它只被调用一次,那么你可以获得 .NET 4 Lazy<T>实现并执行以下操作:

static ConcurrentDictionary<string, Lazy<CacheItem>> Cache = 
    new ConcurrentDictionary<string, Lazy<CacheItem>>(StringComparer.Ordinal);
...
CacheItem item = Cache.GetOrAdd(key, 
               new Lazy<CacheItem>(()=> ExpensiveLoad(key))
             ).Value;

在此版本中,可能会创建多个Lazy<CacheItem>实例,但实际上只会在字典中存储一个实例。 ExpensiveLoad将在第一次取消引用字典中存储的实例Lazy<CacheItem>.Value时调用。此Lazy<T>构造函数使用 LazyThreadSafetyMode.ExecutionAndPublication ,它在内部使用锁,因此确保只有一个线程调用工厂方法ExpensiveLoad

顺便说一句,当使用字符串键构造任何字典时,我总是使用 IEqualityComparer<string> 参数(通常是 StringComparer.Ordinal 或 StringComparer.OrdinalIgnoreCase(来明确记录有关区分大小写的意图。

到目前为止,

我看不到任何大问题。我在您的代码中唯一看不到的是如何公开CacheKeys?最简单的一个是 IList<string>ReadOnlyCollection填充。这样,您的使用者可以很容易地使用索引运算符或 count 属性。在这种情况下,也不需要 volatile 关键字,因为您已经将所有内容放入锁中。所以我会像下面这样拉皮条你的课:

public class CacheItem
{
    public void ExpensiveLoad()
    {
        // some expensive code
    }
}
public class Cache
{
    private static object _SynchObj = new object();
    private static Dictionary<string, CacheItem> _Cache = new Dictionary<string, CacheItem>();
    private static ReadOnlyCollection<string> _CacheKeysReadOnly = new ReadOnlyCollection(new List<string>());
    public IList<string> CacheKeys
    {
        get
        {
            return _CacheKeysReadOnly;
        }
    }
    public CacheItem Get(string key)
    {
        CacheItem item = null;
        ReadOnlyCollection<string> keys = _CacheKeysReadOnly;
        if (!keys.Contains(key))
        {
            lock (_SynchObj)
            {
                keys = _CacheKeysReadOnly;
                if (!keys.Contains(key))
                {
                    item = new CacheItem();
                    item.ExpensiveLoad();
                    _Cache.Add(key, item);
                    List<string> newKeys = new List<string>(_CacheKeysReadOnly);
                    newKeys.Add(key);
                    _CacheKeysReadOnly = newKeys.AsReadOnly();
                }
            }
        }
        return item;
    }
}

作为替代方案,如果您已经在 .Net 4.5 上,您还可以考虑对 CacheKeys 属性使用 IReadOnlyList<T> 接口。

最新更新