制作与QUOT;修改 - 而-枚举"采集线程安全线程、安全、QUOT

2023-09-03 16:25:37 作者:男人善騙,女人善變

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

I want to create a thread-safe collection that can be modified while being enumerated.

样品 ActionSet 类存储动作处理程序。它具有添加方法,增加了一个新的处理程序列表和调用方法,列举并调用所有的收集到的动作处理程序。预期的工作方案包括非常频繁枚举偶尔修改,同时列举。

The sample ActionSet class stores Action handlers. It has the Add method that adds a new handler to the list and the Invoke method that enumerates and invokes all of the collected action handlers. The intended working scenarios include very frequent enumerations with occasional modifications while enumerating.

普通收藏抛出异常,如果您使用添加方法,而枚举还没有结束对其进行修改。

Normal collections throw exception if you modify them using the Add method while the enumeration is not over.

有一个简单的,但速度缓慢问题的解决方法:只要克隆收集统计前:

There is an easy, but slow solution to the problem: Just clone the collection before enumeration:

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

使用此解决方案的问题是枚举开销和我想枚举来非常快。

The problem with this solution is the enumeration overhead and I want enumeration to be very fast.

我已经创建了一个相当快的递归安全的集合,它允许添加即使枚举新值。如果您在主 _actions 集合被枚举添加新值,该值被添加到临时 _delta 集合,而不是的主要原因之一。毕竟枚举完成后, _delta 值将被添加到 _actions 集合。如果你添加了一些新的价值,而主要的 _actions 集合被枚举(创建 _delta 集合),然后重新再次输入Invoke方法,我们需要创建一个新的合并集合( _actions + _delta )和替换 _actions 吧。

I've created a rather fast "recursion-safe" collection that allows adding new values even while enumerating. If you add new values while the main _actions collection is being enumerated, the values are added to the temporary _delta collection instead of the main one. After all enumerations are finished, the _delta values are added to the _actions collection. If you add some new values while the main _actions collection is being enumerated (creating the _delta collection) and then re-enter the Invoke method again we have to create a new merged collection (_actions + _delta) and replace _actions with it.

那么,这个集合是递归安全的,但我想让它线程安全的。我想,我需要使用互锁。* 结构,由的System.Threading 类和其他同步原语,使这个集合线程安全的,但我没有对如何做一个好主意。

So, this collection looks "recursion-safe", but I want to make it thread-safe. I think that I need to use the Interlocked.* constructs, classes from System.Threading and other synchronization primitives to make this collection thread-safe, but I don't have a good idea on how to do that.

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

How to make this collection thread-safe?

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

推荐答案

下面是你们班改装的线程安全的:

Here is your class modified for thread safety:

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

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

I made a few other tweaks, for the following reason:

颠倒IF EX pressions先有恒定值,所以如果我做了 错字和把=,而不是==或!=等,编译器会 立即告诉错字我。 (:一种习惯我钻进了,因为我的大脑和手指往往是不同步的:) preallocated _delta,并称为 .Clear(),而不是将其设置为null, 因为我觉得这样更易于阅读。 各种锁(_sync){...} 给你的所有的实例变量访问你的线程安全。 :(除您访问_action枚举本身): reversed IF expressions to have constant value first, so if I do a typo and put "=" instead of "==" or "!=" etc., the compiler will instantly tell me of the typo. (: a habit I got into because my brain and fingers are often out of sync :) preallocated _delta, and called .Clear() instead of setting it to null, because I find it is easier to read. the various lock(_sync) {...} give you your thread safety on all instance variable access. :( with the exception of your access to _action in the enumeration itself. ):