Over a million developers have joined DZone.
{{announcement.body}}
{{announcement.title}}

Making Sense of Thread Synchronization in C#

DZone's Guide to

Making Sense of Thread Synchronization in C#

Learn what a thread race condition is and one way to avoid it in C# to restrict access to a shared resource and make sure it is correctly synchronized.

· Performance Zone ·
Free Resource

Sensu is an open source monitoring event pipeline. Try it today.

A "thread race condition" is when two threads are trying to write to a shared resource simultaneously, and access to your shared resource has not been correctly synchronized. It is one of the most common problems that can occur when creating multi-threaded software and is notoriously difficult to debug since it might occur once every 10,000 times you run your program. Hence, using your debugger to sort out these types of problems in your code is like using a hammer and a saw to try to fix your car. To illustrate the problem, imagine the following rally taking place somewhere in an alternative universe, where thread synchronization is impossible, due to the nature of e=mc2+1...

Image title

The .NET framework contains lots of helper classes to make sure we have synchronized access to our shared resources, but the intricacies of using these classes, to make sure you are accessing shared resources (correctly) often makes your code much more verbose and difficult to understand. Littering your code with tons of "synchronization code" also makes it much less readable.

At my day job, I needed to create a guarantee of that I could access a shared resource correctly. The problem I was facing was how to access the Asterisk Management API using AsterNET and consume it in a web application. This, of course, implies that multiple threads might be accessing the same socket connection since each HTTP request to my web application will (obviously) be running in a separate thread. I had to synchronize my access to my AsterNET "Manager" instance. I chose to create a wrapper API class, wrapping my AsterNET Manager class, which again, I needed to synchronize access to.

The single Manager instance I needed to create in my web application has two types of methods: methods that change the state of my Asterisk server and methods that read data from my Asterisk server. I figured a "ReaderWriterLockSlim" instance from the .NET Framework would adequately do the job.

However, littering my code with hundreds of "EnterReadLock", "ExitReadLock", "EnterWriteLock" and "ExitWriteLock" invocations seemed like a suicide solution to me, waiting to blow up in my face. I asked myself if I could create a more intuitive and generalized way to access this shared resource, in such a way that my coworkers would have a guarantee of that their access was always synchronized, resulting in a guarantee of that no race conditions would ever occur. In addition, I wanted to create an API for accessing my module that made it literally impossible to create thread synchronization issues. Since my code is consumed by multiple different software developers, sometimes with varying understanding of multi-threaded programming and its problems, making it dead simple to consume my code without creating thread synchronization issues became paramount! Enter my "Synchronizer" class...

/// <summary>
/// Encapsulates an instance to type "T", such that accessing this
/// instance becomes literally IMPOSSIBLE without first entering
/// some sort of "lock".
/// </summary>
class Synchronizer<T>
{
    public delegate void SynchroniserDelegater(T shared);
    ReaderWriterLockSlim _lock = new ReaderWriterLockSlim();
    T _shared;

    /// <summary>
    /// CTOR that completely hides the shared resource for you.
    /// </summary>
    /// <param name="shared">Shared.</param>
    public Synchronizer(T shared)
    {
        _shared = shared;
    }

    /// <summary>
    /// Enters a read lock, and allows you to gain access to your shared
    /// resource in your own (anonymous?) delegate.
    /// </summary>
    /// <param name="functor">Functor.</param>
    public void Read (SynchroniserDelegater functor)
    {
        _lock.EnterReadLock();
        try {
            functor(_shared);
        } finally {
            _lock.ExitReadLock();
        }
    }

    /// <summary>
    /// Enters a write lock, and allows you to gain access to your shared
    /// resource in your own (anonymous?) delegate.
    /// </summary>
    /// <param name="functor">Functor.</param>
    public void Write(SynchroniserDelegater functor)
    {
        _lock.EnterWriteLock();
        try {
            functor(_shared);
        } finally {
            _lock.ExitWriteLock();
        }
    }
}

The idea with the above Synchronizer class is that once you have created your shared resource, you encapsulate it inside of an instance of a Synchronizer, which literally makes it impossible for you to access your shared resource without first entering some sort of "Read" or "Write" lock. For my particular AsterNET problem, this implies creating one instance of my shared AsterNET Manager wrapper in (for instance) my Global.asax.cs file's ApplicationStartup event, and never allowing an HTTP request to access my shared resource directly, only indirectly through my Synchronizer instance, which "wraps" my Manager wrapper instance. Use of the above class can be illustrated by imagining the following class.

/// <summary>
/// Some dummy class, where instances of this class might exist,
/// where the same instance might be possibly shared among multiple threads
/// </summary>
class Shared
{
    string _foo = "initial value";

    public string Read()
    {
        return _foo;
    }

    public void Write(string someData)
    {
        _foo += someData;
    }
}

The idea with the above (dummy) class is that instances of it might be shared among multiple threads, such as my problem with my AsterNET Manager wrapper instance. To illustrate the problem, imagine the following Console application, which creates 3 threads, where each of my threads are accessing the same instance of my above Shared class.

/// <summary>
/// A simple Console program that creates 3 threads, where each thread
/// is accessing a shared resource, either in "Read" mode or in "Write" mode.
/// </summary>
class MainClass
{
    public static void Main(string[] args)
    {
        /*
         * Creating our Synchronizer, that ensures synchronised access to a
         * single "Shared" instance.
         * 
         * Notice, instead of keeping a reference to our "Shared" instance,
         * we completely hide our Shared instance, and encapsulate it as a
         * field, inside of our "Synchronizer" instance, which gives us a
         * guarantee of that we're never able to access our "Shared" object,
         * unless we first enter either a "Read" lock or a "Write" lock,
         * providing us with a guarantee of that all access to our shared
         * resource is always synchronised.
         * 
         * This literally makes it IMPOSSIBLE to access our "Shared" instance,
         * without first entering some sort of "lock" first, resulting in that
         * race conditions becomes literally impossible.
         */
        var synchronizer = new Synchronizer<Shared>(new Shared());

        /*
         * Creating a couple of threads, all accessing the same shared instance.
         * However, since our "Shared" instance is completely encapsulated
         * inside of our "Synchronizer" instance, accessing this "Shared"
         * instance becomes literally IMPOSSIBLE without first entering
         * some sort of lock first.
         *
         * The first thread we create requires only "Read" access to our
         * "Shared" instance. Hence, we use the "Read" method on our
         * "Synchronizer" instance to gain access to our "Shared" instance.
         */
        string foo_from_thread_1 = "";
        var thread1 = new Thread(new ThreadStart(delegate {

            /*
             * This is where the magic of our Synchronizer instance occurs, since
             * this will enter a "read lock" of our ReaderWriterLockSlim instance,
             * which is a field in our Synchronizer class.
             * 
             * Hence, after we enter our "Read" delegate below, no "Write" invocations
             * will be allowed to execute their delegate, in any other threads,
             * before our "Read" delegate has finished executing.
             */
            synchronizer.Read(delegate(Shared shared) {

                /*
                 * Notice!
                 *
                 * Inside of this delegate, you should NEVER "write" to
                 * the shared object, whatever that implies for your class.
                 *
                 * See the "Caveat 1" further down in the article for a potential
                 * generic fix for this problem.
                 */
                foo_from_thread_1 = shared.Read();
            });
        }));

        /*
         * Creating another thread, consuming our shared resource, but this
         * time in "Write" mode.
         */
        var thread2 = new Thread(new ThreadStart(delegate {

            // Entering a "Write" lock.
            synchronizer.Write(delegate(Shared shared) {

               /*
                * Notice!
                *
                * Inside of this delegate, you can both read and write
                * to your shared instance, whetever that implies for your
                * particular class.
                */
                shared.Write(" added in thread2");
            });
        }));

        /*
         * Creating another thread, consuming our shared resource, but this
         * time in "Read" mode.
         */
        string foo_from_thread_3 = "";
        var thread3 = new Thread(new ThreadStart(delegate {

            /*
             * Entering a "Read" lock.
             */
            synchronizer.Read(delegate(Shared shared) {
                foo_from_thread_3 = shared.Read();
            });
        }));

        /*
         * Starting all of our threads.
         */
        thread1.Start();
        thread2.Start();
        thread3.Start();

        /*
         * Waiting for all threads to finish.
         */
        thread1.Join();
        thread2.Join();
        thread3.Join();

        /*
         * Spitting out results from thread 1 and 3 on the Console.
         *
         * Notice, thread2 didn't read any data, so hence only thread1
         * and thread2 actually have any results.
         */
        Console.WriteLine(string.Format("Thread 1 value '{0}'", foo_from_thread_1));
        Console.WriteLine(string.Format("Thread 3 value '{0}'", foo_from_thread_3));
    }
}

The idea with the above "Synchronizer" class, is that every time you need some sort of instance of an object that might be shared among multiple threads, you completely "hide" your shared instance inside your Synchronizer. Since accessing your shared instance is literally impossible without first entering some sort of thread lock, race conditions become (at least in theory) impossible! And once you have accessed your shared resource, you can pass your instance around (inside of the same thread) directly. So basically, your threads can never access the shared resource directly, only accessing it by invoking the "Read" or "Write" method on your Synchronizer first. After you have an instance to your shared resource, you can (of course) pass it around inside of your thread as you see fit.

Hence, the above Synchronizer class's sole purpose, arguably, becomes to make your synchronization code cleaner and more readable, and decrease the likelihood that synchronization issues occur, literally by exploiting the semantics of the CLR, OOP, and the syntax of C#. However, the beautified syntax, even though it arguably provides no additional value over hand coding your "EnterReadLock", "ExitReadLock", "EnterWriteLock" and "ExitWriteLock" invocations — due to its beautified syntax and API, is definitely worth it.

Code is all about explicitly and consistently declaring our intentions in such a way that other human beings can understand our intentions and code. If that wasn't the case, we'd be hand-coding our software in Assembly, without even using an Assembly editor, literally typing "0" and "1" values directly into the memory of our computers.

Caveat 1

Notice, you are still responsible for making sure to never invoke a Write method on your shared resource, inside a Read delegate. The way I solved this for my specific problem was by creating two interfaces: one called "IReadConnection" and another called "IWriteConnection," both of them implemented on my AsterNET Manager instance wrapper class, where my "IWriteConnection" interface inherited from my "IReadConnection" interface. This allowed me to pass in an instance to IReadConnection to my Synchronizer's Read method, and an IWriteConnection instance to my Synchronizer's Write method, to implement the IWriteConnection on the class that wrapped my (single) AsterNET Manager instance.

However, since the above logic cannot be (easily) generalized, and hence is arguably dependent upon the API of your actual shared instance, I chose to completely exclude it in my above sample code, to illustrate the concept without any "cognitive noise" besides the bare minimum requirements of implementing my Synchronizer class. However, if you wish to create a generalized solution with two different interfaces for "Read" and "Write" operations, the general idea is that you could provide two additional generic type parameters when instantiating your "Synchronizer" and pass in your shared resource as an "IReadSomething" to your Read method, and pass your shared resource in as an "IWriteSomething" to your "Write" method, which makes it impossible to write to your shared instance without doing a cast from an "IReadSomething" to an "IWriteSomething" — at which point, if you're doing this, you should probably be whipped around in your office by your CTO throwing "Beginner's Guide to C#" instances in your face!

Caveat 2

The second caveat you must be aware of is that once you have acquired an instance to your shared object (the "Shared" instance in our above code), you should not recursively invoke your Synchroniser instance's Read or Write methods inside of another Read or Write delegate. Hence, once you have (inside of a thread) access to your shared instance, you should pass it around directly, not pass around your Synchronizer instance, to avoid deadlocks where you try to invoke Write inside of a Read delegate, etc. However, at least during debugging, the .NET framework will actually throw an exception if you try to do this, which makes it less likely that you, or any of your coworkers, might end up creating code that somehow does this.

There might be intelligent language/OOP/CLR mechanisms to avoid this problem. However, I haven't been able to come up with any I feel are intelligent enough to write about (yet). If you have ideas for how to solve this, feel free to write a comment, at which point I might revisit the problem next weekend...

In case you didn't get my above joke, here's its explanation...

Image title

Sensu: workflow automation for monitoring. Learn more—download the whitepaper.

Topics:
multithreading ,c# ,performance ,threads ,tutorial

Opinions expressed by DZone contributors are their own.

{{ parent.title || parent.header.title}}

{{ parent.tldr }}

{{ parent.urlSource.name }}