使“修改时枚举”集合成为线程安全的
我想创建一个可以在枚举时修改的线程安全集合。
示例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
来快速搜索元素。