C# 做一个;在枚举时修改";收集线程安全

C# 做一个;在枚举时修改";收集线程安全,c#,.net,collections,concurrency,thread-safety,C#,.net,Collections,Concurrency,Thread Safety,我想创建一个线程安全的集合,可以在枚举时进行修改 示例ActionSet类存储Action处理程序。它具有将新处理程序添加到列表的Add方法和枚举并调用所有收集的操作处理程序的Invoke方法。预期的工作场景包括非常频繁的枚举,并在枚举时偶尔进行修改 如果在枚举未结束时使用Add方法修改正常集合,则会引发异常 有一个简单但缓慢的解决方案:只需在枚举之前克隆集合: class ThreadSafeSlowActionSet { List<Action> _actions = n

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

示例
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();
        }
    }
}

更新:添加了
ThreadSafeSlowActionSet
变体。

以下是为线程安全而修改的类:

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();
            }
        }
    }
}
类安全操作集
{
对象_sync=新对象();
List _actions=new List();//主存储
List _delta=new List();//在枚举主存储时用于存储添加值的临时缓冲区
int _lock=0;//并发调用枚举数
公共无效添加(操作)
{
锁定(同步)
{
如果(0==\u锁)
{//\u操作列表未枚举,可以修改
_动作。添加(动作);
}
其他的
{//\u操作列表正在枚举,无法修改
_delta.Add(action);//将新值存储在_delta缓冲区中
}
}
}
公共无效调用()
{
锁定(同步)
{
如果(0<_增量计数)
{//调用Add后重新输入Invoke:Invoke->Add,Invoke
断言(0<\u锁);
var newActions=newlist(_actions);//为合并增量创建新列表
newActions.AddRange(_delta);//合并delta
_delta.Clear();
_actions=newActions;//替换原始列表(仍在迭代)
}
++_锁;
}
foreach(var动作在_动作中)
{
动作();
}
锁定(同步)
{
--_锁;
如果((0==\u锁定)&(0<\u增量计数))
{
_actions.AddRange(_delta);//合并delta
_delta.Clear();
}
}
}
}
出于以下原因,我做了一些其他调整:

  • 如果表达式首先具有常量值,那么如果执行 键入并放置“=”而不是“==”或“!=”等,编译器将 马上告诉我打字错误。 (:我养成了一个习惯,因为我的大脑和手指经常不同步:)
  • 预分配_delta,并调用.Clear(),而不是将其设置为null, 因为我发现它更容易阅读
  • 各种锁(_sync){…}为所有实例变量访问提供线程安全性。 :(除了访问枚举本身中的_操作之外):
一种更简单的方法(例如,
ConcurrentBag
使用)是让
GetEnumerator()
在集合内容的快照上返回枚举器。在您的情况下,这可能看起来像:

public IEnumerator<Action> GetEnumerator()
{
    lock(sync)
    {
        return _actions.ToList().GetEnumerator();
    }
}
public IEnumerator GetEnumerator()
{
锁定(同步)
{
返回_actions.ToList().GetEnumerator();
}
}

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

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

需要确认该规范。如果在
Invoke
期间调用了
Add
,那么添加的
操作应在
Invoke
的那一轮执行,随后执行或未指定(即,以确定的为准),并且。。我们是否保证所有后续的
Invoke
将调用之前添加的所有操作?您是否检查了并发名称空间@tia?规范非常简单。假设每次调用调用并枚举副本时都复制列表。
ActionSet
应该具有相同的行为(不同之处在于性能会更好,因为不是每次都复制列表-仅在Invoke->Add,Invoke情况下)。因此,1)添加的
操作
将仅在下一次
调用
期间执行。2) 我认为后续的
Invoke
(并发或不并发)应该调用之前添加的所有操作。@CodeIgnoto是的,我知道这些操作,但我有几个问题。我需要经常执行的
Invoke
方法的最佳性能。实际情况几乎不会涉及不同的线程(主要是递归,但发布的版本解决了这个问题)。不过我还是希望集合是线程无关的。如果将
=
而不是
=
放在
条件下(称为),则无需首先将常量放在
中,除非变量是
布尔值,否则编译器将发出警告。啊,Yoda条件!:)对不起,我要离开了。起初,当我第一次看到这个解决方案时,它似乎不是死锁/异常安全的,我基于
readerwriterlocksim
=)创建了一个可怕的200行实现。现在我看到这个解决方案是真正的线程安全的。我怎么可能看不到…@AllonGuralnek-实际上在“C”语言中,如果
x
int
变量,
if(x=5)…
编译得很好,并且总是正确的。而
if(x=0).
编译良好,并且始终为false。哟
public IEnumerator<Action> GetEnumerator()
{
    lock(sync)
    {
        return _actions.ToList().GetEnumerator();
    }
}