使“修改时枚举”集合成为线程安全的

时间:2012-10-27 01:41:47

标签: c# .net collections concurrency thread-safety

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

示例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方法,我们必须创建一个新的合并集合({{1} } + _actions)并用它替换_delta

所以,这个集合看起来“递归安全”,但我想让它具有线程安全性。我认为我需要使用_actions构造,来自Interlocked.*的类和其他同步原语来使这个集合具有线程安全性,但我不知道如何做到这一点。 / p>

如何使这个集合线程安全?

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; } } } 变体。

3 个答案:

答案 0 :(得分:3)

一种更简单的方法(例如,由ConcurrentBag使用)是让GetEnumerator()在集合内容的快照上返回一个枚举器。在您的情况下,这可能看起来像:

public IEnumerator<Action> GetEnumerator()
{
    lock(sync)
    {
        return _actions.ToList().GetEnumerator();
    }
}

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

答案 1 :(得分:1)

以下是针对线程安全性修改的类:

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

我做了一些其他的调整,原因如下:

  • 首先将IF表达式反转为具有常量值,所以如果我这样做的话 错误并输入“=”而不是“==”或“!=”等,编译器会 马上告诉我错字。 (:我养成的习惯,因为我的大脑和手指经常不同步:)。
  • 预分配_delta,并调用 .Clear(),而不是将其设置为null, 因为我觉得它更容易阅读。
  • 各种锁定(_sync){...} 为您提供所有实例变量访问权限的线程安全性。 :(除了您在枚举本身中访问_action之外。):

答案 2 :(得分:0)

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