Click here to Skip to main content
15,860,861 members
Articles / Programming Languages / C#

Thread-safe enumeration in C#

Rate me:
Please Sign up or sign in to vote.
4.85/5 (32 votes)
4 Feb 2010Public Domain4 min read 134.7K   71   29
A way to have thread-safe foreach statements without explicit locking.

Introduction

The IEnumerable interface in C# is a really useful abstraction. Introduction of LINQ in .NET 3.0 made it even more versatile. However, in a multithreaded environment, using it is wrought with peril, as the underlying collection may be modified anytime, instantly destroying your foreach loop or LINQ-expression.

I am going to suggest a simple trick that will make creating thread-safe enumerators super-easy, by strategically using another great interface: IDisposable.

Problems with iteration

As MSDN correctly states, "enumerating through a collection is intrinsically not a thread-safe procedure". Even if you use a synchronized collection (or one of the concurrent collections in .NET 4.0), and all methods use a lock statement internally, iteration using foreach may still fail. Another thread can change the collection when the control is inside the foreach loop - and thus, the collection is not locked - and bam, InvalidOperationException!

Traditionally, this problem is solved by wrapping the loop in a lock statement like this:

C#
lock(collection.SyncRoot){
    foreach(var item in collection){
        // do stuff
    }
}

The problem with this approach is that the locking object is public, and anyone anywhere can lock on it. And, that is just inviting deadlocks.

Another way to have iteration in a thread-safe way is to simply make a copy of a collection:

C#
foreach(var item in collection.Clone()){
    // do stuff
}

That assumes that the Clone() method is thread-safe. Even when it is, this pattern cannot boast high performance - we're actually iterating twice through the whole collection, to say nothing of allocating and then garbage-collecting memory for a clone.

Certainly, a much better way would be to write something like:

C#
foreach(var item in collection.ThreadSafeEnumerator()){
    // do stuff
}

and have it be automatically thread-safe like in the first example. This is how to achieve this.

Creating a thread-safe enumerator

A great thing about foreach (and, by extension, LINQ) is that it correctly executes the Dispose() method of the enumerator. That is, foreach actually becomes a try-finally statement, where the enumerator is created, then iterated inside a try block, and disposed in finally. The IEnumerator<T> interface is actually inherited from IDisposable; its non-generic counterpart is not (because in .NET 1.0, foreach didn't work like that).

Taking advantage of this, we will create an enumerator that enters a lock in the constructor, and exits in Dispose(). This way, the collection will stay locked throughout the entire iteration, and no rogue thread will be able to change it.

C#
public class SafeEnumerator<T>: IEnumerator<T>
{
    // this is the (thread-unsafe)
    // enumerator of the underlying collection
    private readonly IEnumerator<T> m_Inner;
    // this is the object we shall lock on. 
    private readonly object m_Lock;

    public SafeEnumerator(IEnumerator<T> inner, object @lock)
    {
        m_Inner = inner;
        m_Lock = @lock;
        // entering lock in constructor
        Monitor.Enter(m_Lock);
    }

    #region Implementation of IDisposable

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

    #endregion

    #region Implementation of IEnumerator

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

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

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

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

    #endregion
}

This is a trivial enumerator that enters lock on creation and exits on disposal. To actually use it, we must create a collection that uses it. For example:

C#
public class MyList<T>: IList<T>{
    // the (thread-unsafe) collection that actually stores everything
    private List<T> m_Inner;
    // this is the object we shall lock on. 
    private readonly object m_Lock=new object();
    
    IEnumerator<T> IEnumerable<T>.GetEnumerator()
    {
        // instead of returning an usafe enumerator,
        // we wrap it into our thread-safe class
        return new SafeEnumerator<T>(m_Inner.GetEnumerator(), m_Lock);
    }
    
    // To be actually thread-safe, our collection
    // must be locked on all other operations
    // For example, this is how Add() method should look
    public void Add(T item)
    {
        lock(m_Lock)
            m_Inner.Add(item);
    }
    
    // ... the rest of IList<T> implementation goes here
}

This example shows a thread-safe wrapper around List<T>. This wrapper is absolutely synchronized - no other thread can do anything with it when it is used in a foreach loop.

Other neat stuff

Writing a thread-safe wrapper around List<T> (or any other collection) is useful; but we can make it even better. Let's make use of extension methods! First of all, here's a wrapper around IEnumerable<T>:

C#
public class SafeEnumerable<T> : IEnumerable<T>
{
    private readonly IEnumerable<T> m_Inner;
    private readonly object m_Lock;

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

    #region Implementation of IEnumerable

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

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

    #endregion
}

Using this wrapper, we can write an extension for all enumerable collections:

C#
public static class EnumerableExtension
{
    public static IEnumerable<T> AsLocked<T>(this IEnumerable<T> ie, object @lock)
    {
        return new SafeEnumerable<T>(ie, @lock);
    }
}

And now, we can lock any collection by simply using this method:

C#
// in a class...
public class MyThreadSafeEnumerable<T>{
    // come collection of items..
    private IEnumerable<T> m_Items;
    private readonly object m_Lock=new object();
    // and thread-safe getter for them!
    public IEnumerable<T> Items{
        get
        {
            return m_Items.AsLocked(m_Lock);
        }
    }
}
    
// .. or simply in loop
foreach(var item in someList.AsLocked(someLock)){
    // ...
}

Neat, huh? Of course, that last example is in fact the same locking method from the beginning of the article. Still, it's arguably more readable. And, when the lock is made private, it's even more readable and less deadlock-prone.

Additional considerations

While it's all good when using foreach, using this enumerator by explicitly calling collection.GetEnumerator() is more dangerous than before. Forget to call Dispose() on it, and your collection is stuck in a lock forever. Implementing the finalization pattern on the enumerator might help with it, but really, the way to go is to never use GetEnumerator() unless absolutely necessary. And really, don't use it even then.

Also, it must be noted that even a private lock object doesn't guarantee deadlock-free code. As code inside a foreach loop may be arbitrary, one can still manage to deadlock. For example, like this:

C#
// thread 1:
foreach(var item in SafeCollection){
    // do stuff
    lock(SomeObject){
        // do other stuff
    }
}

// thread 2:
lock(SomeObject){
    // do stuff
    SafeCollection.Add(foo); // <-Deadlock!
}

Here, the first thread locks the collection, then tries to enter the lock on SomeObject.. which is held by the second thread, and that waits on the collection's lock to add something to it. So, the lock is never released, and the threads hang up. Deadlocks are tricky like that. To remedy such a situation, you can add timeout to Monitor.Enter() in the constructor, and throw an exception if the timeout expired. Exception is still not a correct behaviour for a program, but it's arguably better than a deadlock - and certainly easier to debug!

Another possible upgrade to the thread-safe enumerator is using ReaderWriterLock (or even better, ReaderWriterLockSlim) in place of Monitor. As iterations do not change the collection, it makes sense to allow many concurrent iterations at once, and only block concurrent changes to the collection. This is what ReaderWriterLock is for!

License

This article, along with any associated source code and files, is licensed under A Public Domain dedication


Written By
Software Developer (Senior)
Russian Federation Russian Federation
This member has not yet provided a Biography. Assume it's interesting and varied, and probably something to do with programming.

Comments and Discussions

 
QuestionIs this in a NuGet package somewhere? Or do we have to implement it ourselves? Pin
rhyous1-Aug-22 11:26
rhyous1-Aug-22 11:26 
AnswerRe: Is this in a NuGet package somewhere? Or do we have to implement it ourselves? Pin
Jeppe Salomon Hvidkjær Clausen14-May-23 11:24
Jeppe Salomon Hvidkjær Clausen14-May-23 11:24 
QuestionDispose ? Pin
Berni27-May-20 20:06
Berni27-May-20 20:06 
QuestionProblem with Insert(int index, T item) Pin
Dismember30-Oct-19 0:10
Dismember30-Oct-19 0:10 
QuestionI don't see how you resolved problem with concurrency Pin
aldema16-Apr-15 7:52
aldema16-Apr-15 7:52 
BugNo thread safe constructor Pin
Jochen Axt14-Jul-13 23:23
Jochen Axt14-Jul-13 23:23 
GeneralMy vote of 5 Pin
Jochen Axt14-Jul-13 21:15
Jochen Axt14-Jul-13 21:15 
QuestionThis is really great. Pin
essence14-Feb-13 8:05
essence14-Feb-13 8:05 
QuestionNice One! Pin
Aron Kovacs22-Nov-12 23:19
Aron Kovacs22-Nov-12 23:19 
Questioncan you give some exmples using this code? Pin
gil_adino8-Jul-12 21:04
gil_adino8-Jul-12 21:04 
AnswerRe: can you give some exmples using this code? Pin
Alexey Drobyshevsky8-Jul-12 21:32
Alexey Drobyshevsky8-Jul-12 21:32 
GeneralRe: can you give some exmples using this code? Pin
gil_adino10-Jul-12 1:53
gil_adino10-Jul-12 1:53 
GeneralMy vote of 5 Pin
tonyf888829-Dec-11 6:18
tonyf888829-Dec-11 6:18 
QuestionVery helpful, I made a small change Pin
tonyf888829-Dec-11 5:54
tonyf888829-Dec-11 5:54 
GeneralRe: Very helpful, I made a small change Pin
Member 943881217-Mar-14 7:32
Member 943881217-Mar-14 7:32 
GeneralBug in the code - fixable though Pin
ByteGuru4-Apr-14 20:37
ByteGuru4-Apr-14 20:37 
GeneralI must be missing something. Pin
jgauffin9-Feb-10 7:15
jgauffin9-Feb-10 7:15 
GeneralRe: I must be missing something. Pin
Alexey Drobyshevsky9-Feb-10 7:36
Alexey Drobyshevsky9-Feb-10 7:36 
GeneralRe: I must be missing something. Pin
jgauffin9-Feb-10 7:39
jgauffin9-Feb-10 7:39 
GeneralYes, yes, yes! Pin
Josh Fischer5-Feb-10 9:20
Josh Fischer5-Feb-10 9:20 
GeneralCloning may be better Pin
supercat95-Feb-10 5:45
supercat95-Feb-10 5:45 
GeneralRe: Cloning may be better Pin
Josh Fischer5-Feb-10 9:25
Josh Fischer5-Feb-10 9:25 
GeneralRe: Cloning may be better Pin
supercat95-Feb-10 9:58
supercat95-Feb-10 9:58 
JokeNotes Pin
kosat4-Feb-10 10:15
kosat4-Feb-10 10:15 
GeneralRe: Notes Pin
Alexey Drobyshevsky4-Feb-10 20:59
Alexey Drobyshevsky4-Feb-10 20:59 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.