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); } }
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.
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:
Calls ModifyNumericField
with 10.
Calls ModifyNumericField
with 15.
Calls ReadNumericField
and gets 15, not 10.
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);