Safe Thread Synchronization-Threading
Safe Thread Synchronization-Threading
Jeffrey Richter
Contents
The Great Idea
Implementing the Great Idea
Using Monitor to Manipulate a SyncBlock
Synchronizing the Microsoft Way
Simplifying the Code with the C# Lock Statement
Synchronizing Static Members the Microsoft Way
Why the Great Idea isn't So Great
Unboxed Instances of Value Types
By far, the most common use of thread synchronization is to ensure mutually exclusive access to a shared resource by multiple threads. In the Win32®
API, the CRITICAL_SECTION structure and associated functions offers the fastest and most efficient way to synchronize threads for mutually
exclusive access when the threads are all running in a single process. The Microsoft® .NET Framework doesn't expose a CRITICAL_SECTION
structure, but it does offer a similar mechanism allowing mutually exclusive access among a set of threads running in the same process. This
mechanism is made possible by way of SyncBlocks and the System.Threading.Monitor class.
In this column, I'm going to explain how this common use of thread synchronization is exposed via the .NET Framework. Specifically, I'm going to
explain the motivation for why SyncBlocks and Monitors were designed the way they are and how they work. Then, at the end of this column, I'm
going to explain why this design is horrible and show you how to use this mechanism in a good, safe fashion.
public:
SomeType() {
// The constructor initializes the
// object's CRITICAL_SECTION field
InitializeCriticalSection(&m_csObject);
}
~SomeType() {
// The destructor deletes the
// object's CRITICAL_SECTION field
DeleteCriticalSection(&m_csObject);
}
void SomeMethod() {
// In the methods, we use the object's
// CRITICAL_SECTION field to synchronize
// access to the object by multiple threads.
EnterCriticalSection(&m_csObject);
// Execute thread-safe code here...
LeaveCriticalSection(&m_csObject);
}
void AnotherMethod() {
// In the methods, we use the object's
// CRITICAL_SECTION field to synchronize
Page 1 of 8
// access to the object by multiple threads.
EnterCriticalSection(&m_csObject);
// Execute thread-safe code here...
LeaveCriticalSection(&m_csObject);
}
};
When you call the Enter method, it first checks to see if the specified object's SyncBlockIndex is negative and if it is, the method finds a free
SyncBlock and records its index in the object's SyncBlockIndex. Once a SyncBlock is associated with the object, this method examines the specified
object's SyncBlock to see if another thread currently owns the SyncBlock. If it is currently unowned, then the calling thread becomes the owner of the
object. If, on the other hand, another thread owns the SyncBlock when Enter is called, then the calling thread is suspended until the currently owning
thread gives up ownership of the SyncBlock.
If you want to code more defensively, then instead of calling Enter, you can call one of the following TryEnter methods:
public static Boolean TryEnter(object obj);
Page 2 of 8
int millisecondsTimeout);
The first version simply checks whether the calling thread can gain ownership of the object's SyncBlock and returns true if it is successful. The other
two methods allow you to specify a timeout value indicating how long you'll allow the calling thread to sit around idly and wait for ownership. All the
methods will return false if ownership cannot be obtained.
Once ownership is obtained, the code can access the object's fields safely. When finished, the thread should release the SyncBlock by calling Exit:
public static void Exit(object obj);
If the calling thread doesn't own the specified object's SyncBlock, then Exit will throw a SynchronizationLockException. Also note that a thread can
own a SyncBlock recursively; every successful call to Enter/TryEnter must be matched by a corresponding call to Exit before the SyncBlock is
considered unowned.
Page 3 of 8
class Transaction {
// Simple function
public void SomeMethod() {
// Lock the object
lock (this) {
// Return
}
In addition to this simplification, the lock statement ensures that Monitor.Exit is called, releasing the SyncBlock even if an exception occurs inside the
try block. You should always use exception handling with thread synchronization mechanisms to ensure that locks are released properly. If you use the
C# lock statement, the compiler writes the proper code for you automatically. By the way, Visual Basic® .NET has a SyncLock statement that does the
same things as the C# lock statement.
Page 4 of 8
As it turns out, the block of memory that contains a type's type descriptor is an object in the heap. Figure 2 doesn't show it, but the SomeType Type
Descriptor and AnotherType Type Descriptor memory blocks are actually objects themselves and, as such, each has a MethodTablePointer field and a
SyncBlockIndex field. This means that a SyncBlock can be associated with a type and a reference to the type object can be passed to the Monitor's
Enter and Exit methods. In the version of the Transaction class shown in Figure 6, all of the members have been changed to static and the
PerformTransaction method and LastTransaction property have been modified to show how Microsoft expects developers to synchronize access to
static members.
Figure 6 New Transaction Class
class Transaction {
In the code for the method and property, you no longer see the this keyword being used since it can't be referenced in static members. Instead, I'm
passing a reference to the type's type descriptor object to the lock statement. This reference is obtained using the C# typeof operator—this operator
returns a reference to the specified type's type descriptor. In Visual Basic .NET, the same functionality is exposed via the GetType operator.
class App {
static void Main() {
// Construct an instance of the App object
App a = new App();
Page 5 of 8
// root to this object and force a garbage collection
a = null;
GC.Collect();
Fortunately, there is a solution to this problem. However, it means you must ignore the Microsoft design and recommendations. Instead, you must
define a private System.Object field as a member of your type, construct the object, and then use the C# lock or Visual Basic .NET SyncLock
statement passing in a reference to the private object. Figure 8 shows how to rewrite the Transaction class so that the object used for synchronization
is private to the class object. Likewise, Figure 9 shows how to rewrite the Transaction class where all the members are static.
Figure 9 Transaction with Static Members
class Transaction {
Page 6 of 8
// Private field holding the time of
// the last transaction performed
private DateTime timeOfLastTransaction;
It seems odd to have to construct a System.Object object just for synchronization with the Monitor class. When you get right down to it, I feel that
Microsoft designed the Monitor class improperly. It should have been designed so that you construct an instance of the Monitor type for each type that
you intend to synchronize. Then, the static methods should have been instance methods that don't require the use of the System.Object parameter. This
would have solved all these problems and would have substantially simplified the programming model for developers.
By the way, if you create complex types with many fields, your methods and properties may need to lock only a subset of the object's fields at any
time. You can always lock specific fields by passing the specific field object to lock or pass to Monitor.Enter. Of course, I would only consider doing
this if the fields are private (which I always recommend). If you have several fields that you want to lock together, you can either use one of the fields
as the one you always pass to lock or Enter. Or, you can construct a System.Object object that you use for the sole purpose of locking a field set. The
more finely grained your locking, the better performance and scalability your code will achieve.
You might be surprised to learn that in this code, no thread synchronization occurs! The reason is that flag is an unboxed value type, not a reference
type. Instances of unboxed value types do not have the two overhead fields, MethodTablePointer and SyncBlockIndex. This means that an unboxed
value type instance can't have a SyncBlock associated with it.
Monitor's Enter and Exit methods require a reference to an object on the heap. When C#, Visual Basic .NET, and many other compilers see code that is
trying to pass an unboxed value type instance to a method that requires an object reference, they automatically generate code to box the instance. The
boxed instance will have a MethodTablePointer and a SyncBlockIndex so the boxed version can be used for thread synchronization. However, a new
boxed instance is created each time a method is called and therefore different objects are being locked and unlocked.
For example, in the last code snippet, when the Flag property's set property accessor method is called, it calls Monitor's Enter method. Enter requires a
reference type and so flag is boxed and the pointer to the boxed version is passed to Enter. This boxed object's SyncBlock is now owned by the calling
thread. If another thread were to access this property now, then flag would be boxed again making a new object with its own SyncBlock. In addition,
the calls to Exit also box the passed value type.
As I said, it took me several hours to discover this problem. If you want to synchronize access to an unboxed value type instance, then you must
Page 7 of 8
allocate a System.Object object and use it for synchronization. The code in Figure 10 is the corrected code.
Figure 10 Now There's Synchronization
class AnotherType {
By the way, if you use the C# lock statement instead of calling Monitor's Enter and Exit methods directly, then the C# compiler will protect you from
accidentally trying to lock a value type. When you pass an unboxed value type instance to the lock statement, the C# compiler produces an error. For
example, if you try to pass a Boolean (bool in C#) to the lock statement, the following error is produced: error CS0185: 'bool' is not a reference type as
required by the lock statement. The Visual Basic .NET compiler also reports the following error if you attempt to use an unboxed value type instance
with its SyncLock statement: error BC30582: 'SyncLock' operand cannot be of type 'Boolean' because 'Boolean' is not a reference type.
Page 8 of 8