Home > Articles > Programming > C#

More Effective C#: Item 13: Use lock() As Your First Choice for Synchronization

  • Print
  • + Share This
  • 💬 Discuss
This chapter is from the book
Bill Wagner shows how to provide a safe way for different threads in your application to send and receive data with each other by using synchronization primitives to protect access to the shared data.

Threads need to communicate with each other. Somehow, you need to provide a safe way for different threads in your application to send and receive data with each other. However, sharing data between threads introduces the potential for data integrity errors in form of synchronization issues. Somehow you need to be certain that the current state of every shared data item is consistent. You achieve this safety by using synchronization primitives to protect access to the shared data. Synchronization primitives ensure that the current thread will not be interrupted until a critical set of operations is completed.

There are many primitives available in the .NET BCL that you can use to safely ensure that access to shared data is synchronized. Only one of them, Monitor.Enter() / Monitor.Exit(), was given special status in the C# language. Monitor.Enter() and Monitor.Exit() implement a critical section block. Critical sections are such a common synchronization technique that the language designers added support for them using the lock() statement. You should follow that example and make lock() the primary tool for synchronization.

The reason is simple: The compiler generates consistent code, and you may make mistakes some of the time. The C# language introduces the lock keyword to control synchronization for multi-threaded programs. The lock statement generates exactly the same code as if you used Monitor.Enter( ) and Monitor.Exit( ) correctly. It’s just easier and it automatically generates all the exception safe code you need.

However, there are two conditions where Monitor gives you necessary control that you can’t get when you use lock(). lock is lexically scoped. That means you can’t enter a Monitor in one lexical scope and exit it in another when using the lock statement. You wouldn’t be able to enter a monitor in a method and exit it inside a lambda expression defined in that method. (See Item 37). The second reason is that Monitor.Enter supports a timeout, which I’ll cover later in this item.

You can lock any reference type with the lock statement:

public int TotalValue
{
    get 
    { 
        lock(syncHandle)
        { 
            return total;
        }
    }
}

public void IncrementTotal()
{
    lock (syncHandle)
    {
        total++;
    }
}

The lock statement gets the exclusive monitor for an object and ensures that no other thread can access the object until the lock is released. The code above using lock() generates the same IL as the following version using Monitor.Enter() and Monitor.Exit():

public void IncrementTotal()
{
    object tmpObject = synchHandle;
    System.Threading.Monitor.Enter(tmpObject);
    try
    {
        total++;
    }
    finally
    {
        System.Threading.Monitor.Exit(tmpObject);
    }
}

The lock statement provides many checks that will help you avoid common mistakes. lock checks that the type being locked is a reference type, as opposed to a value type. Monitor.Enter does not include such safeguards. This routine, using lock() won’t compile:

public void IncrementTotal()
{
    lock (total) // compiler error: can’t lock value type
    {
        total++;
    }
}

But this will:

public void IncrementTotal()
{
    // really doesn’t lock total
    // locks a box containing total.
    Monitor.Enter(total); 
    try
    {
        total++;
    }
    finally
    {
        // Might throw exception
        // unlocks a different box containing total
        Monitor.Exit(total); 
    }
}

Monitor.Enter() compiles because it’s official signature takes a System.Object. total can be coerced into an object by boxing it. Monitor.Enter() actually locks the box containing total. That’s where the first bug lurks. Imagine thread 1 enters IncrementTotal() and acquires a lock. Then, while incrementing the total, the second thread calls IncrementTotal(). Thread 2 now enters IncrementTotal() and acquires the lock. It succeeds in acquiring a different lock, because total gets put in a different box. Thread 1 has a lock on one box containing the value of total. Thread 2 has a lock on another box containing the value of total. You’ve got extra code in place and no synchronization.

Then, you’ll get bit by the second bug: when either thread tries to release the lock on total, the Monitor.Exit() method throws a SynchronizationLockException. That’s because total goes into yet another box to coerce it into the method signature for Monitor.Exit, which also expects a System.Object type. When you release the lock on this box, you unlock a resource that was different than the resource that was used for the lock. Monitor.Exit() fails and throws an exception.

Of course, some bright soul will try this:

public void IncrementTotal()
{
    // doesn’t work either:
    object lockHandle = total;
    Monitor.Enter(lockHandle); 
    try
    {
        total++;
    }
    finally
    {
        Monitor.Exit(lockHandle); 
    }
}

This version won’t throw any exceptions, but won’t provide any synchronization protection either. Each call to IncrementTotal() creates a new box, and acquires a lock to that object. Every thread succeeds in immediately acquiring the lock, but it’s not a lock on a shared resource. Every thread wins, and total is not consistent.

There are more subtle errors that lock prevents as well. Enter() and Exit() are two separate calls, so you can easily make the mistake of acquiring and releasing different objects. This may cause a SynchronizationLockException. But, if you happen to have a type that locks more than one synchronization object, you could possibly have a situation where you had acquired two different locks in a thread, and released the wrong one at the end of a critical section.

The lock statement automatically generates exception safe code, something many of us humans forget. Also, it generates more efficient code, because it only needs to evaluate the target object once. So, by default, you should use the lock statement to handle the synchronization needs in your C# programs.

There is one limitation to the fact that lock generates the same MSIL as Monitor.Enter( ) though. Monitor.Enter() waits forever to acquire the lock. You have introduced a possible deadlock condition. In large enterprise systems, you may need to be more defensive in how you attempt to access critical resources. Monitor.TryEnter( ) lets you specify a timeout for an operation, and attempt a workaround when you can’t access a critical resource.

public void IncrementTotal()
{
    if (!Monitor.TryEnter(syncHandle, 1000)) // wait 1 second
        throw new PreciousResourceException
            ("Could not enter critical section");
    try
    {
        total++;
    }
    finally
    {
        Monitor.Exit(syncHandle);
    }
}

You can wrap this technique in a handy little generic class:

public sealed class LockHolder<T> : IDisposable where T : class
{
    private T handle;
    private bool holdsLock;

    public LockHolder(T handle, int milliSecondTimeout)
    {
        this.handle = handle;
        holdsLock = System.Threading.Monitor.TryEnter(
            handle, milliSecondTimeout);
    }

    public bool LockSuccessful
    {
        get { return holdsLock; }
    }

    #region IDisposable Members
    public void Dispose()
    {
        if (holdsLock)
            System.Threading.Monitor.Exit(handle);
        // Don’t unlock twice
        holdsLock = false;
    }
    #endregion
}

You would use this in the following manner:

object lockHandle = new object();

using (LockHolder<object> lockObj = new LockHolder<object>

    (lockHandle, 1000))
{
    if (lockObj.LockSuccessful)
    {
        // work elided
    }
}
// Dispose called here.

The C# team added implicit language support for Monitor.Enter() and Monitor.Exit() pairs in the form of the lock statement because it is the most common synchronization technique that you will use. The extra checks that the compiler can take on your behalf will make it easier to create synchronization code in your application. Therefore, lock() is the best choice for most synchronization between threads in your C# applications.

However, lock is not the only choice for synchronization. In fact, when you are synchronization access to numeric types, or replacing a reference, the System.Threading.Interlocked class provides support for synchronizing single operations on objects. System.Threading.Interlocked has a number of methods that you can use to access shared data such that a given operation completes before any other thread can access that location. It will also give you a healthy respect for the kinds of synchronization issues that arise when you work with shared data.

Consider this method:

public void IncrementTotal()
{
    total++;
}

As written, interleaved access could lead to an inconsistent representation of the data. An increment operation is not a single machine instruction. The value of total must be fetched from main memory and stored in a register. The value of the register must be incremented. The new value from the register must be stored back into the proper location in main memory. Another thread reading the value after the first thread grabs the value from main memory but before storing the new value back causes data inconsistency. Suppose two different threads interleave calls to IncrementTotal,. Thread A reads the value of 5 from total. At that moment, the active thread switches to Thread B. Thread B reads the value of 5 from total, then increments it, and stores 6 back in the value of total. At this moment, the active thread switches back to thread A. Thread A now increments the register value to 6, and stores that value back in total. IncrementTotal() has been called twice, once by thread A, and once by Thread B, but because of untimely interleaved access, the end effect is that only one update has occurred. These errors are hard to find because they require a interleaved access at the exact wrong moment.

You could use lock() to synchronize this operation, but there is a better way. The Interlocked class has a simple method that fixes the problem: InterlockedIncrement. By rewriting IncrementTotal this way, the increment onperation cannot be interrupted and both increment operations will always be recorded:

public void IncrementTotal()
{
    System.Threading.Interlocked.Increment(ref total);
}

The Interlocked class contains other methods to work with built in data types. Interlocked.Decrement() decrements a value. Interlocked.Exchange() would switch a value with a new value and return the current value. You’d use it to set new state and return the previous state. For example, you might want to store the user ID of the last user to access a resource. You could call Interlocked.Exchange() to store the current user ID, while at the same time retrieving the previous user ID.

Finally, there is the CompareExchange() method. CompareExchange reads the value of a piece of shared data, and if the value matches a sought value, CompareExchange will update it. Otherwise, nothing happens. In either case, it returns the previous value stored at that location. Item 14 shows how to use CompareExchange to create a private lock object inside a class using CompareExchange().

The Interlocked class and lock() are not the only synchronization primitives available to you. The Monitor class also includes Pulse and Wait methods to implement a consumer / producer design. You can also use a ReaderWriterLockSlim class for those designs that have many threads accessing a value that few threads are modifying. ReaderWriterLockSlim contains several improvements over the earlier version of ReaderWriterLock. You should use ReaderWriterLockSlim for all new development.

For most common synchronization problems, examine the Interlocked class to see if it can be used to provide the capabilities you need. With many single operations, it can. When it can’t your first choice is the lock() statement. Only look beyond those when you need some special purpose locking capability.

  • + Share This
  • 🔖 Save To Your Account

Discussions

comments powered by Disqus