使"modify-while-enumerating"集合线程安全



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

样本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,

lock locks of删除/插入和果在枚举期间更改了该集合,则不会抱怨。我还添加了Dictionary以使元素搜索快速。

最新更新