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.
|