6

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

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.

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.

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

Update: Added the ThreadSafeSlowActionSet variant.

Ark-kun
  • 6,358
  • 2
  • 34
  • 70
  • Need to confirm the specification for that. If `Add` is called during `Invoke` so that added `action` should be executed in that turn of `Invoke`, executed later or unspecified (i.e. whichever is ok) – tia Oct 27 '12 at 02:23
  • And.. do we guarantee that all subsequent `Invoke` will invoke all action that previously added? – tia Oct 27 '12 at 02:28
  • have you checked the concurrent namespace http://msdn.microsoft.com/en-us/library/system.collections.concurrent.aspx – Prabhu Murthy Oct 27 '12 at 02:39
  • @tia The specification is very easy. Imagine that the list is copied each time the Invoke is called and the copy is enumerated. The `ActionSet` should have the same behavior (the difference is that the performance would be much better, because the list is not copied every time - only in the Invoke->Add,Invoke situations). So, 1) The added `action` will only be executed during next `Invoke`. 2) I think that subsequent `Invoke` (concurrent or not) should invoke all actions added previously. – Ark-kun Oct 27 '12 at 02:56
  • @CodeIgnoto Yes, I know about them, but I have a couple of problems with them. I need the best performance of the `Invoke` method which is executed very often. The real situations will almost never involve different threads (mostly recursion, but the posted version solves that case). Still I want the collection to be thread-proof. – Ark-kun Oct 27 '12 at 03:00
  • Have a look at 'System.Threading.Interlocked.Increment(ref xxx);' if you are modifying critical integer values in threaded code. – Jon Rea Oct 29 '12 at 09:54
  • @JonRea Thanks. I know about the Interlocked.Increment. But it doesn't save me here. There are checks and so on. It would take much more to make this code thread-safe. – Ark-kun Oct 30 '12 at 10:47
  • Are you only ever going to need to add elements? – Eamon Nerbonne Dec 17 '12 at 13:23
  • @EamonNerbonne Actually I need to delete them too. (I'm re-implementing MulticastDelegate.) – Ark-kun Dec 17 '12 at 16:29

3 Answers3

3

A simpler approach (used, for example, by ConcurrentBag) is to have GetEnumerator() return an enumerator over a snapshot of the collection's contents. In your case this might look like:

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

If you do this, you don't need a _delta field and the complexity it adds.

Joe
  • 122,218
  • 32
  • 205
  • 338
  • For the OPs question with frequent enumerations, you might want to cache the `ToList()` result in a thread safe manner and invalidate the cache for any `Add`/`Remove`. A little more complexity, but still better than the starting point. – Damien_The_Unbeliever Dec 17 '12 at 13:15
  • @Damien_The_Unbeliever your variant is definitely better than @Joe's variant (which I should have added to the question from the start as the `ThreadSafeSlowActionSet`). Still the modification is still rather common and the list cloning will degrade the performance (most importantly it will cause frequent GC-caused lags on WP7). – Ark-kun Dec 17 '12 at 16:35
1

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:

  • 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. ):
Jesse Chisholm
  • 3,857
  • 1
  • 35
  • 29
  • There's no need to put the constant first in an `if` condition (known as [yoda conditions](http://www.codinghorror.com/blog/2012/07/new-programming-jargon.html)), it won't compile if you put `=` instead of `==` unless the variable is a `Boolean`, in which case the compiler will emit a warning. – Allon Guralnek Dec 17 '12 at 13:11
  • Aaah, Yoda Conditions! :) http://www.codinghorror.com/blog/2012/07/new-programming-jargon.html – Matěj Zábský Dec 17 '12 at 13:25
  • Sorry for zoning off. At first the solution seemed not deadlock/exception safe when I first looked at it and I created a dreadful 200-line implementation based on the `ReaderWriterLockSlim` =). Now I see that this solution is really thread safe. How could I not see that.... – Ark-kun Dec 17 '12 at 17:02
  • @AllonGuralnek - Actually in the "C" language, if `x` is an `int` variable, `if (x = 5) ...` compiles just fine, and is always true. While `if (x = 0) ...` compiles fine and is always false. You are correct that in `C++` itis the case that `x` must be a `Boolean` for `if (x = someExpression ) ...` to be applicable. You may get a warning for putting a constant expression in an if, but an error forces me to fix it, while a warning might slip by. And yes, I do realize that using the compiler to ameliorate my bad habits is itself a bad habit. ;-) – Jesse Chisholm Nov 10 '15 at 16:53
0

Since I actually also needed to delete items from the collection, the implementation that I ultimately used was based on a rewritten LinkedList that locks adjacent nodes on deletion/insertion and doesn't complain if the collection was changed during enumeration. I also added a Dictionary to make the element search fast.

Ark-kun
  • 6,358
  • 2
  • 34
  • 70
  • You only lock adjacent nodes? Did you consider deadlocks and check whether it's actually faster than a per-list lock? – Eamon Nerbonne Dec 18 '12 at 12:03
  • I was locking 3 nodes (previous, current and next) checking in a loop until `previous.Next == current`. This order should prevent deadlocks. – Ark-kun Dec 18 '12 at 20:19
  • But you are right. I forgot that I actually threw that code away. I wanted to create a general thread-safe single/double linked list and then wrap it in a class with a `Dictionary` for fast access. I then noticed that I'd still need to lock the whole structure since I had to lock the `Dictionary`. So I removed all locks and general functionality from the linked list classes and started locking only the main `Dictionary`-based part. – Ark-kun Dec 18 '12 at 20:38