Synchronization in Generic Collections [Brian Grunkemeyer]


A few astute users of our generic collections observed that we no longer provide a public SyncRoot property on ICollection<T>.  I believe we’re better off not providing it.  You often want to do synchronization at some higher level in your application, not just on one collection. I’ve recently been writing an FxCop rule that modifies two or three generic collections stored in static variables, and all of these updates need to be atomic. So I take a lock on one synchronization object explicitly allocated for this purpose.

One benefit here is I never have to worry about breaking any invariants in my code between the multiple collections. Say I have the invariant that if List A contains an object, then Dictionary B must also contain that object. If I took a lock on A then updated A & released the lock, then took a lock on B, updated B & released the lock, I’d have a window where my invariant wasn’t true when viewed from other threads. That’s not the right thing to do, and our synchronized wrapper design pattern encouraged this by allowing you to write naïve code that would update just one collection safely.

Worse yet, the synchronized wrapper pattern led some MS developers to write code like this to check whether a queue was empty:

    if (queue.Count > 0) {

        Object obj = null;

        try {

            obj = queue.Dequeue();

        }

        catch (Exception) { } // This swallows any indication we have a race

        // Use obj here, dealing with null. }

    }

Here there is a lock around both the Count property and the Dequeue method, but the lock is released by our synchronized wrapper in between these two method calls. The right fix (using our non-generic collections) is something more like this:

    Object obj = null;

    lock (queue.SyncRoot) {

        // In this app, locking on the queue was sufficient

        if (queue.Count > 0)

            obj = queue.Dequeue();

    }

    // use obj here, dealing with null.

 

To be fair, our design patterns forced this on people. If we train people to just pretend a synchronized wrapper will solve their threading problems, we’re setting up our users to fall into subtle traps like this.

How does a flawed design for synchronized wrappers imply we should remove ICollection’s SyncRoot property? Remember that the SyncRoot property only existed to implement these synchronized wrappers. People can still use lock(myCollection) or lock(myInternalSyncObject).

I think we’re better off forcing people to think more heavily about synchronization, and frankly a SyncRoot property on our generic collections doesn’t provide any value.

Comments (13)

  1. Anonymous says:

    I agree wholeheartedly 🙂

  2. Anonymous says:

    Um. There are still wrappers like readonly wrappers and other wrappers which people outside Microsoft might want to create. Hard to believe i know but sometimes people do things you guys don’t anticipate which is why APIs should be as flexible as possible.

    Without SyncRoot, a wrapper W, around collection C won’t necessarily be thread safe if both W and the naked instance C are still concurrently accessible from different threads. This depends on how complex the functionality "W" adds to "C" is but you never know and without SyncRoot, it will be very hard for anyone to synchronise access to both C and W in an abstract way. Either the user of W needs to know about C and synchronise that (which breaks the abstraction W gives the user over C) or the user of C will have to know about W (also breaks abstraction since C should be oblivious to being wrapped) or there needs to be a third object O in which both the users of C and W need to synchronize which creates too much nasty coupling.

  3. Anonymous says:

    But I do agree that synchronization wrappers are really nasty cause they do give a false sense of security. Usually synchronisation requires large blocks of operations to be "atomic-ised" rather than a single method call (like you demonstrated).

  4. As you know generics were only introduced to the .NET Framework in version 2.0. Originally we had a lot

  5. A common problem users run into when writing parallel applications is the lack of the thread-safety support

  6. In my last post we discussed the problems with designing a safer API for mutable thread safe collections

  7. Recently I was involved in a discussion about generic collections and thread-safety. I was under the impression the framework had thread-safe versions of the generic collections, but I was unable to locate them and resorted to Google. I was wrong. There