使“修改时枚举”集合成为线程安全的

我想创建一个可以在枚举时修改的线程安全集合。

示例ActionSet类存储Action处理程序。 它有Add方法,它为列表添加一个新的处理程序,以及枚举和调用所有收集的操作处理程序的Invoke方法。 预期的工作方案包括非常频繁的枚举,在枚举时偶尔进行修改。

如果在枚举未结束时使用Add方法修改它们,则正常集合会抛出exception。

这个问题有一个简单但缓慢的解决方案:在枚举之前克隆集合:

 class ThreadSafeSlowActionSet { List _actions = new List(); public void Add(Action action) { lock(_actions) { _actions.Add(action); } } public void Invoke() { lock(_actions) { List actionsClone = _actions.ToList(); } foreach (var action in actionsClone ) { action(); } } } 

这个解决方案的问题是枚举开销,我希望枚举非常快。

我创建了一个相当快速的“递归安全”集合,允许在枚举时添加新值。 如果在枚举main _actions集合时添加新值, _actions将值添加到临时_delta集合而不是主集合。 完成所有枚举后, _delta值将添加到_actions集合中。 如果在枚举main _actions集合时添加一些新值(创建_delta集合),然后再次重新输入Invoke方法,我们必须创建一个新的合并集合( _actions + _delta )并用它替换_actions

所以,这个集合看起来“递归安全”,但我想让它成为线程安全的。 我认为我需要使用Interlocked.*构造,来自System.Threading和其他同步原语的类来使这个集合成为线程安全的,但我不知道如何做到这一点。

如何使这个集合线程安全?

 class RecursionSafeFastActionSet { List _actions = new List(); //The main store List _delta; //Temporary buffer for storing added values while the main store is being enumerated int _lock = 0; //The number of concurrent Invoke enumerations public void Add(Action action) { if (_lock == 0) { //_actions list is not being enumerated and can be modified _actions.Add(action); } else { //_actions list is being enumerated and cannot be modified if (_delta == null) { _delta = new List(); } _delta.Add(action); //Storing the new values in the _delta buffer } } public void Invoke() { if (_delta != null) { //Re-entering Invoke after calling Add: Invoke->Add,Invoke Debug.Assert(_lock > 0); var newActions = new List(_actions); //Creating a new list for merging delta newActions.AddRange(_delta); //Merging the delta _delta = null; _actions = newActions; //Replacing the original list (which is still being iterated) } _lock++; foreach (var action in _actions) { action(); } _lock--; if (_lock == 0 && _delta != null) { _actions.AddRange(_delta); //Merging the delta _delta = null; } } } 

更新 :添加了ThreadSafeSlowActionSet变体。

以下是针对线程安全性修改的类:

 class SafeActionSet { Object _sync = new Object(); List _actions = new List(); //The main store List _delta = new List(); //Temporary buffer for storing added values while the main store is being enumerated int _lock = 0; //The number of concurrent Invoke enumerations public void Add(Action action) { lock(sync) { if (0 == _lock) { //_actions list is not being enumerated and can be modified _actions.Add(action); } else { //_actions list is being enumerated and cannot be modified _delta.Add(action); //Storing the new values in the _delta buffer } } } public void Invoke() { lock(sync) { if (0 < _delta.Count) { //Re-entering Invoke after calling Add: Invoke->Add,Invoke Debug.Assert(0 < _lock); var newActions = new List(_actions); //Creating a new list for merging delta newActions.AddRange(_delta); //Merging the delta _delta.Clear(); _actions = newActions; //Replacing the original list (which is still being iterated) } ++_lock; } foreach (var action in _actions) { action(); } lock(sync) { --_lock; if ((0 == _lock) && (0 < _delta.Count)) { _actions.AddRange(_delta); //Merging the delta _delta.Clear(); } } } } 

我做了一些其他的调整,原因如下:

  • 反向IF表达式首先具有常量值,所以如果我输入错误并输入“=”而不是“==”或“!=”等,编译器会立即告诉我输入错误。 (:我养成的习惯是因为我的大脑和手指经常不同步:)
  • preallocated _delta,并调用.Clear()而不是将其设置为null,因为我发现它更容易阅读。
  • 各种锁(_sync){...}为您提供所有实例变量访问的线程安全性。 :(除了您在枚举本身中访问_action之外。):

一种更简单的方法(例如,由ConcurrentBag )是让GetEnumerator()在集合内容的快照上返回一个枚举器。 在您的情况下,这可能看起来像:

 public IEnumerator GetEnumerator() { lock(sync) { return _actions.ToList().GetEnumerator(); } } 

如果这样做,则不需要_delta字段及其添加的复杂性。

因为我实际上还需要从集合中删除项目,所以我最终使用的实现基于重写的LinkedList 在删除/插入时锁定相邻节点 如果在枚举期间更改了集合,则不会抱怨。 我还添加了一个Dictionary来快速搜索元素。