【发布时间】:2012-10-17 06:50:55
【问题描述】:
我想创建一个可以在枚举时修改的线程安全集合。
示例ActionSet 类存储Action 处理程序。它具有将新处理程序添加到列表的Add 方法和枚举并调用所有收集的操作处理程序的Invoke 方法。预期的工作场景包括非常频繁的枚举,在枚举时偶尔会进行修改。
如果您在枚举未结束时使用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期间调用了Add,那么添加的action应该在Invoke的那一回合中执行,稍后执行或未指定(即,无论哪个都可以) -
而且.. 我们是否保证所有后续的
Invoke都会调用之前添加的所有操作? -
你检查并发命名空间msdn.microsoft.com/en-us/library/…
-
@tia 规范非常简单。想象一下,每次调用 Invoke 时都会复制该列表并枚举该副本。
ActionSet应该具有相同的行为(不同之处在于性能会更好,因为不是每次都复制列表 - 仅在 Invoke->Add,Invoke 情况下)。所以,1)添加的action只会在下一个Invoke期间执行。 2)我认为后续的Invoke(并发与否)应该调用之前添加的所有操作。 -
@CodeIgnoto 是的,我知道它们,但我对它们有一些问题。我需要经常执行的
Invoke方法的最佳性能。实际情况几乎不会涉及不同的线程(主要是递归,但发布的版本解决了这种情况)。我仍然希望该系列能够防线。
标签: c# .net collections concurrency thread-safety