我想创建一个可以在被枚举时修改的线程安全集合。
样本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
。P>
因此,这个集合看起来"递归安全",但我想使其成为线程安全。我认为我需要使用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;
}
}
}
update :添加了ThreadSafeSlowActionSet
变体。
一种更简单的方法(例如,ConcurrentBag
使用)是让GetEnumerator()
通过该集合内容的快照返回枚举器。在您的情况下,这可能看起来像:
public IEnumerator<Action> GetEnumerator()
{
lock(sync)
{
return _actions.ToList().GetEnumerator();
}
}
如果这样做,则不需要_delta字段及其添加的复杂性。
以下是您的类已修改为线程安全:
class SafeActionSet
{
Object _sync = new Object();
List<Action> _actions = new List<Action>(); //The main store
List<Action> _delta = new List<Action>(); //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<Action>(_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();
}
}
}
}
我进行了其他一些调整,出于以下原因:
- 是否首先具有恒定值的表达方式,因此,如果我做一个typo和put" ="而不是" =="或"!="等,编译器将立即告诉我错字。(:我之所以养成的习惯,因为我的大脑和手指通常是不同步的:)
- preallosated _delta,并称为 .clear(),而不是将其设置为null,因为我发现阅读更容易。
- 各种锁(_sync){...} 在 all> all> all 实例变量访问上为您提供线程安全。:(除了您在枚举本身中获得_的访问。):
,因为我实际上还需要从集合中删除项目,所以我最终使用的实现是基于重写的linkedlist,
Dictionary
以使元素搜索快速。