6

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

示例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变体。

4

3 回答 3

3

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

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

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

于 2012-12-17T13:07:32.180 回答
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,因为我发现它更易于阅读。
  • 各种lock(_sync) {...}为您提供所有实例变量访问的线程安全。:( 除了您在枚举本身中访问 _action 之外。):
于 2012-11-07T05:47:59.947 回答
0

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

于 2012-12-17T17:08:05.647 回答