Thread-safe enumeration in C#

Yan Cui

I help clients go faster for less using serverless technologies.

I had a problem with the project I’m working on at work where on the base class I had a list which its child classes need to have access to in a read-only capacity but manipulation to the list itself is handled by the base class only. However, the standard C# List<T> and Enumerator<T> are not thread-safe and we started seeing problems when the list is modified by one thread whilst another is trying to loop through it.

Whilst looking for a clean solution we found this article on CodeProject:

http://www.codeproject.com/KB/cs/safe_enumerable.aspx

The article had covered much of the implementation you need, but left some gaps you need to plug yourself for a thread-safe list which I have included below along with some of the useful methods you’d find on List<T>:

Thread-safe Enumerator<T>

/// <summary>
/// A thread-safe IEnumerator implementation.
/// See: http://www.codeproject.com/KB/cs/safe_enumerable.aspx
/// </summary>
public class SafeEnumerator<T>: IEnumerator<T>
{
    // this is the (thread-unsafe)
    // enumerator of the underlying collection
    private readonly IEnumerator<T> _inner;

    // this is the object we shall lock on.
    private readonly object _lock;

    public SafeEnumerator(IEnumerator<T> inner, object @lock)
    {
        _inner = inner;
        _lock = @lock;

        // entering lock in constructor
        Monitor.Enter(_lock);
    }

    public T Current
    {
        get { return _inner.Current; }
    }

    object IEnumerator.Current
    {
        get { return Current; }
    }

    public void Dispose()
    {
        // .. and exiting lock on Dispose()
        // This will be called when foreach loop finishes
        Monitor.Exit(_lock);
    }

    /// <remarks>
    /// we just delegate actual implementation
    /// to the inner enumerator, that actually iterates
    /// over some collection
    /// </remarks>
    public bool MoveNext()
    {
        return _inner.MoveNext();
    }

    public void Reset()
    {
        _inner.Reset();
    }
}

Thread-safe List<T>

/// <summary>
/// A thread-safe IList implementation using the custom SafeEnumerator class
/// See: http://www.codeproject.com/KB/cs/safe_enumerable.aspx
/// </summary>
public class SafeList<T> : IList<T>
{
    // the (thread-unsafe) collection that actually stores everything
    private readonly List<T> _inner;

    // this is the object we shall lock on.
    private readonly object _lock = new object();

    public SafeList()
    {
        _inner = new List<T>();
    }

    public int Count
    {
        get
        {
            lock (_lock)
            {
                return _inner.Count;
            }
        }
    }

    public bool IsReadOnly
    {
        get { return false; }
    }

    public T this[int index]
    {
        get
        {
            lock (_lock)
            {
                return _inner[index];
            }
        }
        set
        {
            lock (_lock)
            {
                _inner[index] = value;
            }
        }
    }

    IEnumerator<T> IEnumerable<T>.GetEnumerator()
    {
        lock (_lock)
        {
            // instead of returning an usafe enumerator,
            // we wrap it into our thread-safe class
            return new SafeEnumerator<T>(_inner.GetEnumerator(), _lock);
        }
    }

    /// <remarks>
    /// To be actually thread-safe, our collection must be locked on all other operations
    /// </remarks>
    public void Add(T item)
    {
        lock (_lock)
        {
            _inner.Add(item);
        }
    }

    public void Clear()
    {
        lock (_lock)
        {
            _inner.Clear();
        }
    }

    public bool Contains(T item)
    {
        lock (_lock)
        {
            return _inner.Contains(item);
        }
    }

    public void CopyTo(T[] array, int arrayIndex)
    {
        lock (_lock)
        {
            _inner.CopyTo(array, arrayIndex);
        }
    }

    public bool Remove(T item)
    {
        lock (_lock)
        {
            return _inner.Remove(item);
        }
    }

    public IEnumerator GetEnumerator()
    {
        lock (_lock)
        {
            return new SafeEnumerator<T>(_inner.GetEnumerator(), _lock);
        }
    }

    public int IndexOf(T item)
    {
        lock (_lock)
        {
            return _inner.IndexOf(item);
        }
    }

    public void Insert(int index, T item)
    {
        lock (_lock)
        {
            _inner.Insert(index, item);
        }
    }

    public void RemoveAt(int index)
    {
        lock (_lock)
        {
            _inner.RemoveAt(index);
        }
    }

    public ReadOnlyCollection<T> AsReadOnly()
    {
        lock (_lock)
        {
            return new ReadOnlyCollectio<T>(this);
        }
    }

    [CheckParameters]
    public void ForEach([NotNull] Action<T> action)
    {
        lock (_lock)
        {
            foreach (var item in _inner)
            {
                action(item);
            }
        }
    }

    [CheckParameters]
    public bool Exists([NotNull] Predicate<T> match)
    {
        lock (_lock)
        {
            foreach (var item in _inner)
            {
                if (match(item))
                {
                    return true;
                }
            }
        }
        return false;
    }
}
You can see I’ve used the CheckParameters and NotNull attributes for parameter validation using PostSharp, you can easily substitute them with normal null checks.

Thread-safe IEnumerable<T>

/// <summary>
/// A thread-safe IEnumerable implementation
/// See: http://www.codeproject.com/KB/cs/safe_enumerable.aspx
/// </summary>
public class SafeEnumerable<T> : IEnumerable<T>
{
    private readonly IEnumerable<T> _inner;
    private readonly object _lock;

    public SafeEnumerable(IEnumerable<T> inner, object @lock)
    {
        _lock = @lock;
        _inner = inner;
    }

    public IEnumerator<T> GetEnumerator()
    {
        return new SafeEnumerator<T>(_inner.GetEnumerator(), _lock);
    }

    IEnumerator IEnumerable.GetEnumerator()
    {
        return GetEnumerator();
    }
}

Whenever you’re ready, here are 3 ways I can help you:

  1. Production-Ready Serverless: Join 20+ AWS Heroes & Community Builders and 1000+ other students in levelling up your serverless game.
  2. Consulting: If you want to improve feature velocity, reduce costs, and make your systems more scalable, secure, and resilient, then let’s work together and make it happen.
  3. Join my FREE Community on Skool, where you can ask for help, share your success stories and hang out with me and other like-minded people without all the negativity from social media.

 

3 thoughts on “Thread-safe enumeration in C#”

  1. Just want to point out that your implementation is flawed, try this:

    foreach(int value in safeList)
    {
    safeList.Remove(value);
    }

  2. Just would like to share with others that nowadays this approach is not a good idea. You might not even notice the problem in the beginning but if you use async/await inside the foreach enumeration Monitor.Exit might be called on a different from the Monitor.Enter thread which leads to an unpredictable exception.

Leave a Comment

Your email address will not be published. Required fields are marked *