C#ThreadSafeSortedDictionary使用有限的方法实现

时间:2016-10-11 15:42:36

标签: c# collections .net-4.0 thread-safety

我尝试创建ThreadSafeSortedDictionary,我正在使用.NET 4.0。

我看过Thread safe SortedDictionary但对线程安全方面没有信心。

任何人都认为这可行,或者不是因为我看不到与之相关的原因:

  1. 线程安全 - 它需要是线程安全的
  2. 性能/效率 - 我并不主要关注此事(除非性能存在严重问题)
  3. 我的代码:

    public class ThreadSafeSortedDict<TKey, TValue>
    {
        private readonly SortedDictionary<TKey, TValue> _dict;
        private readonly ReaderWriterLockSlim _dictReaderWriterLockSlim;
        private readonly ReaderWriterLockSlim _readonlyDictionaryLock;
    
        public Dictionary<TKey, TValue> ReadOnly { get; set; }
    
        public ThreadSafeSortedDict(IComparer<TKey> comparer)
        {
            _dict = new SortedDictionary<TKey, TValue>(comparer);
            _dictReaderWriterLockSlim = new ReaderWriterLockSlim();
            _readonlyDictionaryLock = new ReaderWriterLockSlim();
        }
    
        public void Add(TKey key, TValue value)
        {
            _dictReaderWriterLockSlim.EnterWriteLock();
            try
            {
                _dict.Add(key,value);
            }
            finally
            {
                _dictReaderWriterLockSlim.ExitWriteLock();
            }
    
            SetReadOnlyDictionary();
        }
    
        public void AddRange(IEnumerable<KeyValuePair<TKey,TValue>> keyValues)
        {
            if (keyValues == null) return;
            _dictReaderWriterLockSlim.EnterWriteLock();
            try
            {
                foreach (var keyValue in keyValues)
                {
                    Add(keyValue.Key, keyValue.Value);
                }
            }
            finally
            {
                _dictReaderWriterLockSlim.ExitWriteLock();
            }
    
            SetReadOnlyDictionary();
        }
    
        public void Remove(TKey key)
        {
            _dictReaderWriterLockSlim.EnterWriteLock();
            try
            {
                _dict.Remove(key);
            }
            finally
            {
                _dictReaderWriterLockSlim.ExitWriteLock();
            }
    
            SetReadOnlyDictionary();
        }
    
        public void Replace(IEnumerable<KeyValuePair<TKey, TValue>> newKeyValues)
        {
            if (newKeyValues == null) return;
    
            _dictReaderWriterLockSlim.EnterWriteLock();
    
            try
            {
                _dict.Clear();
                AddRange(newKeyValues);
            }
            finally
            {
                _dictReaderWriterLockSlim.ExitWriteLock();
            }
        }
    
        private void SetReadOnlyDictionary()
        {
            _readonlyDictionaryLock.EnterWriteLock();
            try
            {
                ReadOnly = GetSortedKeyValues().ToDictionary(x => x.Key, x => x.Value);
            }
            finally
            {
                _readonlyDictionaryLock.ExitWriteLock();
            }
        }
    
        private List<KeyValuePair<TKey, TValue>> GetSortedKeyValues()
        {
            _dictReaderWriterLockSlim.EnterReadLock();
            try
            {
                return _dict.ToList();
            }
            finally
            {
                _dictReaderWriterLockSlim.ExitReadLock();
            }
        }
    }
    

2 个答案:

答案 0 :(得分:1)

我的代码中没有发现任何明显的线程安全问题。对我来说,更重要的问题是正确性

  1. 一个小问题是您的ReadOnly财产有公共制定者。这意味着类外的任何代码都可以随时设置属性,而无需任何同步。您的课堂设计应反映预期用途。据推测,您不希望任何其他代码更改属性值,因此setter应为private
  2. 主要问题是,虽然班级名称为ThreadSafeSortedDict<TKey, TValue>,但排序的数据无法公开访问。设置ReadOnly属性时,将从原始数据创建新的未排序字典。即使您按顺序枚举源字典(已排序),也无法保证新字典中的数据将保持其添加顺序。根据设计,字典类是无序集合。
  3. 如果您确实想要在SortedDictionary<TKey, TValue>对象中维护数据并显示其排序视图,则需要使用某种类型的有序集合作为ReadOnly属性类型。这可能是另一个SortedDictionary<TKey, TValue>,但由于意图是将集合设置为只读,因此使用IReadOnlyDictionary<TKey, TValue>类型返回ReadOnlyDictionary<TKey, TValue>对象更有意义。而不是每次修改排序字典时创建一个新对象,您可以懒惰地初始化它并在修改排序字典时使其无效。

    例如:

    public class ThreadSafeSortedDict<TKey, TValue>
    {
        private readonly SortedDictionary<TKey, TValue> _dict;
        private IReadOnlyDictionary<TKey, TValue> _readOnly;
        private readonly object _lock = new object();
    
        public IReadOnlyDictionary<TKey, TValue> ReadOnly
        {
            get
            {
                lock (_lock)
                {
                    if (_readOnly == null)
                    {
                      // NOTE: SortedList is faster when populating from already-sorted data
                        _readOnly = new ReadOnlyDictionary(new SortedList(_dict));
                    }
                }
    
                return _readOnly;
            }
        }
    
        public ThreadSafeSortedDict(IComparer<TKey> comparer)
        {
            _dict = new SortedDictionary<TKey, TValue>(comparer);
        }
    
        public void Add(TKey key, TValue value)
        {
            lock (_lock)
            {
                _dict.Add(key,value);
                _readOnly = null;
            }
        }
    
        public void AddRange(IEnumerable<KeyValuePair<TKey,TValue>> keyValues)
        {
            if (keyValues == null) return;
            lock (_lock)
            {
                foreach (var keyValue in keyValues)
                {
                    Add(keyValue.Key, keyValue.Value);
                }
                _readOnly = null;
            }
        }
    
        public void Remove(TKey key)
        {
            lock (_lock)
            {
                _dict.Remove(key);
                _readOnly = null;
            }
        }
    
        public void Replace(IEnumerable<KeyValuePair<TKey, TValue>> newKeyValues)
        {
            if (newKeyValues == null) return;
    
            lock (_lock)
            {
                _dict.Clear();
                AddRange(newKeyValues);
                _readOnly = null;
            }
        }
    }
    

    使用lock代替读写器锁不应该显着影响性能,如果有的话,并使代码更明显地是线程安全的。在我看来,上述应该足够了。不过,如果不是,你可以做的一些改进包括:

    1. 继续使用读写器锁定。您需要获取ReadOnly属性中的阅读器锁定,并在需要设置_readOnly字段时将其升级为编写器锁(即如果它设置为{ {1}})。如果您在null属性上存在大量争用,则可以提高性能。
    2. 不打扰自定义类型,而是使用ReadOnly,只需复制其内容并在需要排序的字典视图后对其进行排序。这是否对你的情况有用,我不能说,因为问题上没有足够的背景。如果您仍然需要实现某种类型的同步以避免在某个其他线程尝试修改集合时尝试检索已排序的视图,那么您可能还需要某种类型的包装器类型。
    3. 让您的类型为ConcurrentDictionary<TKey, TValue>,而不是IReadOnlyDictionary<TKey, TValue>属性。然后,您可以根据需要将对象作为只读字典对象传递,但是需要实际修改字典的代码仍然可以通过原始类型进行。
    4. 鉴于目前问题中有哪些信息,我说第3选项可能是最好的。它需要最多的工作,但在我看来,它提供了最干净,最有效的API。您将避免必须拥有ReadOnly属性,以避免必须复制该集合。类本身将作为只读副本,它将自动保持与基础字典集合相关的最新版本,因为ReadOnly的实现只会遵循基础字典(具有同步) ,当然)。

答案 1 :(得分:0)

首先,感谢@Peter Duniho提供的信息和答案。 我首先尝试使用读写器锁,但遇到了很多麻烦,并建议使用锁以简化。 我的解决方案是修改Peters的答案,因为每次访问 ReadOnly 字典时我都不想锁定

我这堂课的最终目标是:

  1. 在任何时候,我都想访问 ReadOnly 集合(并阻止用户写入)。

  2. 如果添加/删除基础词典中的某些内容,我不希望这会影响已经使用/阅读/迭代他们有权访问的 ReadOnly 的其他人。

  3. 以下是解决方案:

    public class ThreadSafeSortedDictionary<TKey, TValue>
    {
        private readonly SortedDictionary<TKey, TValue> _dict;
        private readonly object _sync;
    
        public ReadOnlyDictionary<TKey, TValue> ReadOnly { get; private set; }
    
        public IEnumerable<TValue> Values
        {
            get { return ReadOnly.Values; }
        }
    
        public IEnumerable<TKey> Keys
        {
            get { return ReadOnly.Keys; }
        }
    
        public int Count
        {
            get { return ReadOnly.Count; }
        }
    
        public ThreadSafeSortedDictionary(IComparer<TKey> comparer)
        {
            _dict = new SortedDictionary<TKey, TValue>(comparer);
            _sync = new object();
        }
    
        public void Add(TKey key, TValue value)
        {
            lock (_sync)
            {
                _dict.Add(key, value);
                SetReadOnlyDictionary();
            }
        }
    
        public void AddRange(IEnumerable<KeyValuePair<TKey, TValue>> keyValues)
        {
            if (keyValues == null) return;
            InternalAddRange(keyValues);
        }
    
        public void Remove(TKey key)
        {
            lock (_sync)
            {
                _dict.Remove(key);
                SetReadOnlyDictionary();
            }
        }
    
        public void Replace(IEnumerable<KeyValuePair<TKey, TValue>> newKeyValues)
        {
            //Throw exception, because otherwise we'll end up clearing the dictionary
            if (newKeyValues == null) throw new ArgumentNullException("newKeyValues");
            InternalAddRange(newKeyValues, true);
        }
    
        private void InternalAddRange(IEnumerable<KeyValuePair<TKey, TValue>> keyValues, bool clearBeforeAdding = false)
        {
            lock (_sync)
            {
                if (clearBeforeAdding) _dict.Clear();
                foreach (var kvp in keyValues)
                {
                    _dict.Add(kvp.Key, kvp.Value);
                }
                SetReadOnlyDictionary();
            }
        }
    
        private void SetReadOnlyDictionary()
        {
            lock (_sync)
            {
                ReadOnly = new DictionaryReadOnly<TKey, TValue>(new SortedList<TKey, TValue>(_dict, _dict.Comparer));
            }
        }
    }
    

    我在Is there a read-only generic dictionary available in .NET?

    的答案中使用了 ReadOnlyDictionary

    我为 ReadOnly 属性使用了 SortedList vs SortedDictionary ,因为我创建了很多次(在添加/删除/替换期间) ),因为 1. SortedList 在内存中表现更好 2.查找速度更快,因为我只是将它们用于只读目的,所以只需要查找。