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

Ark*_*kun 6 .net c# 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();
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

这个解决方案的问题是枚举开销,我希望枚举非常快.

我创建了一个相当快速的"递归安全"集合,允许在枚举时添加新值.如果_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;
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

更新:添加了ThreadSafeSlowActionSet变体.

Jes*_*olm 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();
            }
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

出于以下原因,我做了一些其他调整:

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