简体   繁体   English

这个延迟加载缓存实现线程安全吗?

[英]Is this lazy-loading cache implementation thread-safe?

I'm developing with the 3.5 .NET Framework and I need to use a cache in a multithread scenario with lazy-loading pattern for its items.我正在使用 3.5 .NET Framework 进行开发,我需要在多线程场景中使用缓存,并为其项目使用延迟加载模式。 After reading several articles on the web I've tried to write my own implementation.在阅读了网络上的几篇文章后,我尝试编写自己的实现。

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

As you can see the Cache object uses both a dictionary which stores the real key-value pairs and a list which duplicates only the keys.如您所见,Cache 对象同时使用存储真实键值对的字典和仅复制键的列表。 When a thread calls the Get method it reads the static shared key list (which is declared volatile) and call the Contains method to see if the key is already present and if not uses a double-checked lock pattern before starting the lazy loading.当线程调用 Get 方法时,它读取静态共享密钥列表(声明为 volatile)并调用 Contains 方法以查看密钥是否已经存在,如果不存在,则在开始延迟加载之前使用双重检查锁定模式。 At the end of the loading a new instance of the key list is created and stored in the static variable.在加载结束时,会创建一个新的密钥列表实例并将其存储在静态变量中。

Obviously I'm in a situation where the cost of recreating the entire list of keys is almost irrelevant against the cost of a single item loading.显然,我处于重新创建整个键列表的成本与单个项目加载成本几乎无关的情况。

I hope someone can tell me if it is really thread-safe.我希望有人能告诉我它是否真的是线程安全的。 When I say "thread-safe" I mean that every reader thread can avoid corrupted or dirty reads and every writer thread loads the missing item only once.当我说“线程安全”时,我的意思是每个读取器线程都可以避免损坏或脏读,并且每个写入器线程仅加载一次丢失的项目。

This is not thread safe since you aren't locking when reading the Dictionary.这不是线程安全的,因为您在阅读字典时没有锁定。

There is a race condition whereby one thread can be reading:有一个竞争条件,一个线程可以读取:

return Cache[key];

while another is writing:而另一个正在写:

_Cache.Add(key, item);

As the MSDN documentation for Dictionary<TKey,TValue> states: `正如Dictionary<TKey,TValue>MSDN 文档所述:`

To allow the collection to be accessed by multiple threads for reading and writing, you must implement your own synchronization.要允许多个线程访问集合进行读写,您必须实现自己的同步。

and your synchorization does not include the reader.并且您的同步不包括阅读器。

You really need to use a thread-safe dictionary, which will simplify your code enormously (you won't need the List at all)您确实需要使用线程安全字典,这将极大地简化您的代码(您根本不需要 List)

I'd recommend getting the source for the .NET 4 ConcurrentDictionary.我建议获取 .NET 4 ConcurrentDictionary 的源代码。

Getting thread safety right is hard, as is evidenced by the fact that some of the other answerers are incorrectly stating that your implementation is thread-safe.获得正确的线程安全是很困难的,其他一些回答者错误地说明您的实现是线程安全的就证明了这一点。 Hence I'd trust Microsoft's implementation before a home made one.因此,在自制一个之前,我会相信微软的实施。

If you don't want to use a thread-safe dictionary, then I'd recommend something simple like:如果您不想使用线程安全字典,那么我建议您使用以下简单的方法:

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

You could also try an implementation with a ReaderWriterLockSlim , though you might not get a significant performance improvement (google for ReaderWriterLockSlim performance).您也可以尝试使用ReaderWriterLockSlim实现,尽管您可能不会获得显着的性能改进(谷歌搜索 ReaderWriterLockSlim 性能)。

As for an implementation using a ConcurrentDictionary, in most cases I would simply use something like:至于使用 ConcurrentDictionary 的实现,在大多数情况下,我会简单地使用以下内容:

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

This can result in ExpensiveLoad being called more that once for each key, but I bet if you profile your app you'll find that this is so rare as to not be a problem.这可能会导致每个键多次调用ExpensiveLoad ,但我敢打赌,如果您分析您的应用程序,您会发现这种情况非常罕见,不会成为问题。

If you really insist on ensuring it's only called once, then you could get hold of the .NET 4 Lazy<T> implementation and do something like:如果您真的坚持确保它只被调用一次,那么您可以掌握 .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;

In this version, multiple Lazy<CacheItem> instances might be created, but only one will actually be stored in the dictionary.在这个版本中,可能会创建多个Lazy<CacheItem>实例,但实际上只有一个会存储在字典中。 ExpensiveLoad will be called the first time Lazy<CacheItem>.Value is dereferenced for the instance stored in the dictionary. ExpensiveLoad将在第一次为存储在字典中的实例取消引用Lazy<CacheItem>.Value时被调用。 This Lazy<T> constructor uses LazyThreadSafetyMode.ExecutionAndPublication which uses a lock internally so ensure only one thread calls the factory method ExpensiveLoad .这个Lazy<T>构造函数使用 LazyThreadSafetyMode.ExecutionAndPublication,它在内部使用锁,因此确保只有一个线程调用工厂方法ExpensiveLoad

As an aside, when constructing any dictionary with a string key, I always use the IEqualityComparer<string> parameter (usually StringComparer.Ordinal or StringComparer.OrdinalIgnoreCase) to explicitly document the intention regarding case-sensitivity. IEqualityComparer<string>一句,在使用字符串键构造任何字典时,我总是使用IEqualityComparer<string>参数(通常是 StringComparer.Ordinal 或 StringComparer.OrdinalIgnoreCase)来明确记录有关区分大小写的意图。

So far i can't see any big issues.到目前为止,我看不到任何大问题。 The only thing i can't see in your code is how do you make the CacheKeys public?我在您的代码中唯一看不到的是您如何公开CacheKeys The simplest one would be as IList<string> which is filled by a ReadOnlyCollection .最简单的就是IList<string> ,它由ReadOnlyCollection填充。 This way your consumers can use index operators or the count property quite easily.通过这种方式,您的消费者可以很容易地使用索引运算符或计数属性。 In this case the volatile keyword should also not be needed cause you already put everything into your lock.在这种情况下,也不需要volatile关键字,因为您已经将所有内容放入锁中。 So i would pimp your class as follows:所以我会按如下方式拉皮条你的班级:

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

As an alternative if you already on .Net 4.5 you could also think about using the IReadOnlyList<T> interface for the CacheKeys property.作为替代,如果您已经在 .Net 4.5 上,您还可以考虑将IReadOnlyList<T>接口用于CacheKeys属性。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM