DekGenius.com
[ Team LiB ] Previous Section Next Section

Recipe 15.2 Providing Thread Safe Access to Class Members

Problem

You need to provide thread-safe access through accessor functions to an internal member variable.

The following NoSafeMemberAccess class shows three methods: ReadNumericField, IncrementNumericField and ModifyNumericField. While all of these methods access the internal numericField member, the access is currently not safe for multithreaded access:

public sealed class NoSafeMemberAccess
{
    private NoSafeMemberAccess ( ) {}

    private static int numericField = 1;

    public static void IncrementNumericField( ) 
    {
        ++numericField;
    }

    public static void ModifyNumericField(int newValue) 
    {
        numericField = newValue;
    }

    public static int ReadNumericField( )
    {
        return (numericField);
    }
}

Solution

NoSafeMemberAccess could be used in a multithreaded application, and, therefore, it must be made thread-safe. Consider what would occur if multiple threads were calling the IncrementNumericField method at the same time. It is possible that two calls could occur to IncrementNumericField while the numericField is updated only once. In order to protect against this, we will modify this class by creating an object that we can lock against in critical sections:

public sealed class SaferMemberAccess
{
    private SaferMemberAccess ( ) {}

    private static int numericField = 1;
    private static object syncObj = new object( );

    public static void IncrementNumericField( ) 
    {
        lock(syncObj)
        {
            ++numericField;
        }
    }

    public static void ModifyNumericField(int newValue) 
    {
        numericField = newValue;
    }

    public static int ReadNumericField( )
    {
        int readValue = 0;
        readValue = numericField;
        return (readValue);
    }
}

Using the lock statement on the syncObj object lets us synchronize access to the numericField member. This now makes this method safe for multithreaded access.

Discussion

Marking a block of code as a critical section is done using the lock keyword. This keyword accepts a parameter of either the type object for the class (such as typeof(MyClass)) or a class instance object (new MyClass( )). It uses this type or object to control what you are locking.

There is a problem with synchronization using an object like syncObj in the SaferMemberAccess example. If you lock an object or type that can be accessed by other objects within the application, other objects may also attempt to lock this same object. This will manifest itself in poorly written code that locks itself, such as the following code:

public class DeadLock
{
    public void Method1( )
    {
        lock(this)
        {
            // Do something
        }
    }
}

When Method1 is called, it locks the current DeadLock object. Unfortunately, any object that has access to the DeadLock class may also lock it. This is shown here:

using System;
using System.Threading;

public class AnotherCls
{
    public void DoSomething( )
    {
        DeadLock deadLock = new DeadLock( );
        lock(deadLock)
        {
            Thread thread = new Thread(new ThreadStart(deadLock.Method1));
            thread.Start( );

            // Do some time consuming task here
        }
    }
}

The DoSomething method obtains a lock on the deadLock object and then attempts to call the Method1 method of the deadLock object on another thread, after which a very long task is executed. While the long task is executing, the lock on the deadLock object prevents Method1 from being called on the other thread. Only when this long task ends, and execution exits the critical section of the DoSomething method, will the Method1 method be able to acquire a lock on the this object. As you can see, this can become a major headache to track down in a much larger application.

Jeffrey Richter has come up with a relatively simple method to remedy this situation, which he details quite clearly in the article "Safe Thread Synchronization" in the January 2003 issue of MSDN Magazine. His solution is to create a private field within the class to synchronize on. The object itself can only acquire this private field; no outside object or type may acquire it. The DeadLock class can be rewritten, as follows, to fix this problem:

public class DeadLock
{
    private object syncObj = new object( );

    public void Method1( )
    {
        lock(syncObj)
        {
            // Do something
        }
    }
}

To clean up your code, you should stop locking any objects or types except for the synchronization objects that are private to your type or object, such as the syncObj in the fixed DeadLock class. This recipe makes use of this pattern by creating a static syncObj object within the SaferMemberAccess class. The IncrementNumericField, ModifyNumericField, and ReadNumericField methods use this syncObj to synchronize access to the numericField field. Note that if you do not need a lock while the numericField is being read in the ReadNumericField method, you can remove this lock block and simply return the value contained in the numericField field.

Minimizing the number of critical sections within your code can significantly improve performance. Use what you need to secure resource access, but no more.


If you require more control over locking and unlocking of critical sections, you might want to try using the overloaded static Monitor.TryEnter methods. These methods allow more flexibility by introducing a timeout value. The lock keyword will attempt to acquire a lock on a critical section indefinitely. However, with the TryEnter method, you can enter a timeout value in milliseconds (as an integer) or as a TimeSpan structure. The TryEnter methods return true if a lock was acquired and false if it was not. Note that the overload of the TryEnter method that accepts only a single parameter does not block for any amount of time. This method returns immediately, regardless of whether the lock was acquired.

The updated class using the Monitor methods is shown here:

using System;
using System.Threading;

public sealed class MonitorMethodAccess
{
    private MonitorMethodAccess ( ) {}

    private static int numericField = 1;
    private static object syncObj = new object( );

    public static void IncrementNumericField( ) 
    {
        if (Monitor.TryEnter(syncObj, 250))
        {
            try
            {
                ++numericField;
            }
            finally
            {
                Monitor.Exit(syncObj);
            }
        }
    }

    public static void ModifyNumericField(int newValue) 
    {
        if (Monitor.TryEnter(syncObj, 250))
        {
            try
            {
                numericField = newValue;
            }
            finally
            {
                Monitor.Exit(syncObj);
            }
        }
    }

    public static int ReadNumericField( )
    {
        if (Monitor.TryEnter(syncObj, 250))
        {
            int readValue = 0;

            try
            {
                readValue = numericField;
            }
            finally
            {
                Monitor.Exit(syncObj);
            }

            return (readValue);
        }

        return (-1);
    }

}

Note that with the TryEnter methods, you should always check to see whether the lock was in fact acquired. If it is not, your code should wait and try again, or return to the caller.

You might think at this point that all of the methods are thread-safe. Individually, they are, but what if you are trying to call them and you expect synchronized access between two of the methods? If ModifyNumericField and ReadNumericField are used one after the other by Class 1 on Thread 1 at the same time Class 2 is using these methods on Thread 2, locking or Monitor calls will not prevent Class 2 from modifying the value before Thread 1 reads it. Here is a series of actions that demonstrates this:


Class 1 Thread 1

Calls ModifyNumericField with 10.


Class 2 Thread 2

Calls ModifyNumericField with 15.


Class 1 Thread 1

Calls ReadNumericField and gets 15, not 10.


Class 2 Thread 2

Calls ReadNumericField and gets 15, which it expected.

In order to solve this problem of synchronizing reads and writes, the calling class needs to manage the interaction. The external class could accomplish this by using the Monitor class to establish a lock on the type object, as shown here:

int num = 0;
if(Monitor.TryEnter(typeof(MonitorMethodAccess),250))
{
    MonitorMethodAccess.ModifyNumericField(10);
    num = MonitorMethodAccess.ReadNumericField( );
    Monitor.Exit(typeof(MonitorMethodAccess));
}
Console.WriteLine(num);

See Also

See the "lock Statement," "Thread Class," and "Monitor Class" topics in the MSDN documentation. Also see the "Safe Thread Synchronization" article in the January 2003 issue of MSDN Magazine.

    [ Team LiB ] Previous Section Next Section