我想创建一个线程安全的集合,即使在枚举时也可以修改。
样例代码中的ActionSet类存储Action处理程序。它有Add方法来将新处理程序添加到列表中,还有Invoke方法来枚举并调用收集的所有Action处理程序。预期的工作场景包括频繁枚举和偶尔在枚举过程中修改。
普通集合如果您在枚举尚未结束时使用Add方法进行修改,则会抛出异常。
解决问题的一种简单但慢速的方法是:在枚举之前克隆集合。
这个解决方案的问题在于枚举开销,我希望枚举非常快。
我创建了一个相当快速的“递归安全”集合,即使在枚举时也允许添加新值。如果您在主要的
因此,这个集合看起来是“递归安全”的,但我想使它成为线程安全的。我认为我需要使用
如何使这个集合线程安全?
更新:新增了ThreadSafeSlowActionSet变体。
样例代码中的ActionSet类存储Action处理程序。它有Add方法来将新处理程序添加到列表中,还有Invoke方法来枚举并调用收集的所有Action处理程序。预期的工作场景包括频繁枚举和偶尔在枚举过程中修改。
普通集合如果您在枚举尚未结束时使用Add方法进行修改,则会抛出异常。
解决问题的一种简单但慢速的方法是:在枚举之前克隆集合。
class ThreadSafeSlowActionSet {
List<Action> _actions = new List<Action>();
public void Add(Action action) {
lock(_actions) {
_actions.Add(action);
}
}
public void Invoke() {
lock(_actions) {
List<Action> actionsClone = _actions.ToList();
}
foreach (var action in actionsClone ) {
action();
}
}
}
这个解决方案的问题在于枚举开销,我希望枚举非常快。
我创建了一个相当快速的“递归安全”集合,即使在枚举时也允许添加新值。如果您在主要的
_actions
集合被枚举时添加新值,则这些值将添加到临时的 _delta
集合而不是主要集合中。完成所有枚举后, _delta
值将添加到 _actions
集合中。如果您在主要的 _actions
集合被枚举时添加了一些新值(创建了 _delta
集合),然后再次进入Invoke方法,我们必须创建一个新的合并集合( _actions
+ _delta
)并用其替换 _actions
。因此,这个集合看起来是“递归安全”的,但我想使它成为线程安全的。我认为我需要使用
Interlocked.*
结构、 System.Threading
类和其他同步原语来使这个集合线程安全,但我没有好的想法如何做到这一点。如何使这个集合线程安全?
class RecursionSafeFastActionSet {
List<Action> _actions = new List<Action>(); //The main store
List<Action> _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<Action>();
}
_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<Action>(_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变体。
Invoke
调用会调用之前添加的所有操作吗? - tiaActionSet
应该具有相同的行为(不同之处在于性能会更好,因为列表不会在每次调用中复制 - 仅在Invoke->Add,Invoke情况下才会复制)。所以,1)添加的action
只会在下一个Invoke
中执行。2)我认为后续的Invoke
(并发或非并发)应该调用之前添加的所有操作。 - Ark-kun