Home Random Page


CATEGORIES:

BiologyChemistryConstructionCultureEcologyEconomyElectronicsFinanceGeographyHistoryInformaticsLawMathematicsMechanicsMedicineOtherPedagogyPhilosophyPhysicsPolicyPsychologySociologySportTourism






TheMonitor Class and Sync Blocks

Probably the most-used hybrid thread synchronization construct is the Monitor class, which provides a mutual-exclusive lock supporting spinning, thread ownership, and recursion. This is the most-used construct because it has been around the longest, C# has a built-in keyword to support it, the just-in- time (JIT) compiler has built-in knowledge of it, and the common language runtime (CLR) itself uses

it on your application’s behalf. However, as you’ll see, there are many problems with this construct, making it easy to produce buggy code. I’ll start by explaining the construct, and then I’ll show the problems and some ways to work around these problems.

Every object on the heap can have a data structure, called a sync block, associated with it. A sync block contains fields similar to that of the AnotherHybridLock class that appeared earlier in this chapter. Specifically, it has fields for a kernel object, the owning thread’s ID, a recursion count, and a waiting threads count. The Monitor class is a static class whose methods accept a reference to

 

 

2 Although there is no AutoResetEventSlim class, in many situations you can construct a SemaphoreSlim object with a

maxCount of 1.


any heap object, and these methods manipulate the fields in the specified object’s sync block. Here is what the most commonly used methods of the Monitor class look like.

 

public static class Monitor {

public static void Enter(Object obj); public static void Exit(Object obj);

 

// You can also specify a timeout when entered the lock (not commonly used): public static Boolean TryEnter(Object obj, Int32 millisecondsTimeout);

 

// I’ll discuss the lockTaken argument later

public static void Enter(Object obj, ref Boolean lockTaken);

public static void TryEnter(Object obj, Int32 millisecondsTimeout, ref Boolean lockTaken);

}

 

Now obviously, associating a sync block data structure with every object in the heap is quite wasteful, especially because most objects’ sync blocks are never used. To reduce memory usage, the CLR team uses a more efficient way to offer the functionality just described. Here’s how it works: when the CLR initializes, it allocates an array of sync blocks in native heap. As discussed elsewhere in

this book, whenever an object is created in the heap, it gets two additional overhead fields associated with it. The first overhead field, the type object pointer, contains the memory address of the type’s type object. The second overhead field, the sync block index, contains an integer index into the array of sync blocks.

When an object is constructed, the object’s sync block index is initialized to -1, which indicates that it doesn’t refer to any sync block. Then, when Monitor.Enter is called, the CLR finds a free sync block in the array and sets the object’s sync block index to refer to the sync block that was found. In other words, sync blocks are associated with an object on the fly. When Exit is called, it checks to see whether there are any more threads waiting to use the object’s sync block. If there are no threads waiting for it, the sync block is free, Exit sets the object’s sync block index back to -1, and the free sync block can be associated with another object in the future.



Figure 30-1 shows the relationship between objects in the heap, their sync block indexes, and elements in the CLR’s sync block array. Object-A, Object-B, and Object-C all have their type object pointer member set to refer to Type-T (a type object). This means that all three objects are of the same type. As discussed in Chapter 4, “Type Fundamentals,” a type object is also an object in the heap, and like all other objects, a type object has the two overhead members: a sync block index and a type object pointer. This means that a sync block can be associated with a type object and a refer- ence to a type object can be passed to Monitor’s methods. By the way, the sync block array is able to create more sync blocks if necessary, so you shouldn’t worry about the system running out of sync blocks if many objects are being synchronized simultaneously.


FIGURE 30-1Objects in the heap (including type objects) can have their sync block indexes refer to an entry in the CLR’s sync block array.

 

Here is some code that demonstrates how the Monitor class was originally intended to be used.

 

internal sealed class Transaction { private DateTime m_timeOfLastTrans;

 

public void PerformTransaction() { Monitor.Enter(this);

// This code has exclusive access to the data... m_timeOfLastTrans = DateTime.Now; Monitor.Exit(this);

}

 

public DateTime LastTransaction { get {

Monitor.Enter(this);

// This code has exclusive access to the data... DateTime temp = m_timeOfLastTrans; Monitor.Exit(this);

return temp;

}

}

}


On the surface, this seems simple enough, but there is something wrong with this code. The prob- lem is that each object’s sync block index is implicitly public. The following code demonstrates the impact of this.

 

public static void SomeMethod() { var t = new Transaction();

Monitor.Enter(t); // This thread takes the object's public lock

 

// Have a thread pool thread display the LastTransaction time

// NOTE: The thread pool thread blocks until SomeMethod calls Monitor.Exit! ThreadPool.QueueUserWorkItem(o => Console.WriteLine(t.LastTransaction));

 

// Execute some other code here... Monitor.Exit(t);

}

 

In this code, the thread executing SomeMethod calls Monitor.Enter, taking the Transaction object’s publicly exposed lock. When the thread pool thread queries the LastTransaction property, this property also calls Monitor.Enter to acquire the same lock, causing the thread pool thread

to block until the thread executing SomeMethod calls Monitor.Exit. Using a debugger, you can determine that the thread pool thread is blocked inside the LastTransaction property, but it is very hard to determine which other thread has the lock. If you do somehow figure out which thread has the lock, then you have to figure out what code caused it to take the lock. This is very difficult, and even worse, if you do figure it out, then the code might not be code that you have control over and you might not be able to modify this code to fix the problem. Therefore, my suggestion to you is to always use a private lock instead. Here’s how I’d fix the Transaction class.

 

internal sealed class Transaction {

private readonly Object m_lock = new Object(); // Each transaction has a PRIVATE lock now private DateTime m_timeOfLastTrans;

 

public void PerformTransaction() { Monitor.Enter(m_lock); // Enter the private lock

// This code has exclusive access to the data... m_timeOfLastTrans = DateTime.Now; Monitor.Exit(m_lock); // Exit the private lock

}

 

public DateTime LastTransaction { get {

Monitor.Enter(m_lock); // Enter the private lock

// This code has exclusive access to the data... DateTime temp = m_timeOfLastTrans; Monitor.Exit(m_lock); // Exit the private lock return temp;

}

}

}


If Transaction’s members were static, then simply make the m_lock field static, too, and now the static members are thread safe.

It should be clear from this discussion that Monitor should not have been implemented as a static class; it should have been implemented like all the other locks: a class you instantiate and call instance methods on. In fact, Monitor has many other problems associated with it that are all because it is a static class. Here is a list of additional problems:

■ A variable can refer to a proxy object if the type of object it refers to is derived from the System.MarshalByRefObject class (discussed in Chapter 22, “CLR Hosting and App- Domains”). When you call Monitor’s methods, passing a reference to a proxy object, you are locking the proxy object, not the actual object that the proxy refers to.

■ If a thread calls Monitor.Enter, passing it a reference to a type object that has been loaded domain neutral (discussed in Chapter 22), the thread is taking a lock on that type across all AppDomains in the process. This is a known bug in the CLR that violates the isolation that AppDomains are supposed to provide. The bug is difficult to fix in a high-performance way, so it never gets fixed. The recommendation is to never pass a reference to a type object into Monitor’s methods.

■ Because strings can be interned (as discussed in Chapter 14, “Chars, Strings, and Working with Text”), two completely separate pieces of code could unknowingly get references to a single String object in memory. If they pass the reference to the String object into Monitor’s methods, then the two separate pieces of code are now synchronizing their execution with each other unknowingly.

■ When passing a string across an AppDomain boundary, the CLR does not make a copy of the string; instead, it simply passes a reference to the string into the other AppDomain. This

improves performance, and in theory, it should be OK because String objects are immutable. However, like all objects, String objects have a sync block index associated with them, which is mutable, and this allows threads in different AppDomains to synchronize with each other unknowingly. This is another bug in CLR’s AppDomain isolation story. The recommendation is never to pass String references to Monitor’s methods.

■ Because Monitor’s methods take an Object, passing a value type causes the value type to get boxed, resulting in the thread taking a lock on the boxed object. Each time Monitor.En­ ter is called, a lock is taken on a completely different object and you get no thread synchro- nization at all.

■ Applying the [MethodImpl(MethodImplOptions.Synchronized)] attribute to a method causes the JIT compiler to surround the method’s native code with calls to Monitor.Enter and Monitor.Exit. If the method is an instance method, then this is passed to these meth- ods, locking the implicitly public lock. If the method is static, then a reference to the type’s type object is passed to these methods, potentially locking a domain-neutral type. The recom- mendation is to never use this attribute.


■ When calling a type’s type constructor (discussed in Chapter 8, “Methods”), the CLR takes a lock on the type’s type object to ensure that only one thread initializes the type object and its static fields. Again, this type could be loaded domain neutral, causing a problem. For example, if the type constructor’s code enters an infinite loop, then the type is unusable by all App- Domains in the process. The recommendation here is to avoid type constructors as much as possible or least keep them short and simple.

Unfortunately, the story gets worse. Because it is so common for developers to take a lock, do some work, and then release the lock within a single method, the C# language offers simplified syntax via its lock keyword. Suppose that you write a method like this.

 

private void SomeMethod() { lock (this) {

// This code has exclusive access to the data...

}

}

 

It is equivalent to having written the method like this.

 

private void SomeMethod() { Boolean lockTaken = false; try {

// An exception (such as ThreadAbortException) could occur here... Monitor.Enter(this, ref lockTaken);

// This code has exclusive access to the data...

}

finally {

if (lockTaken) Monitor.Exit(this);

}

}

 

The first problem here is that the C# team felt that they were doing you a favor by calling Monitor.Exit in a finally block. Their thinking was that this ensures that the lock is always released no matter what happens inside the try block. However, this is not a good thing. If an exception occurs inside the try block while changing state, then the state is now corrupted. When the lock is exited in the finally block, another thread will now start manipulating the corrupted state. It is better to have your application hang than it is to continue running with a corrupted state and potential security holes. The second problem is that entering and leaving a try block decreases the performance of the method. And some JIT compilers won’t inline a method that contains a try block in it, which decreases performance even more. So now we have slower code that lets threads access corrupted state.3The recommendation is not to use C#’s lock statement.

Now we get to the Boolean lockTaken variable. Here is the problem that this variable is trying to solve. Let’s say that a thread enters the try block and before calling Monitor.Enter, the thread is aborted (as discussed in Chapter 22). Now the finally block is called, but its code should not exit the lock. The lockTaken variable solves this problem. It is initialized to false, which assumes that the lock has not been entered into. Then, if Monitor.Enter is called and successfully takes the

 
 

3 By the way, while still a performance hit, it is safe to release a lock in a finally block if the code in the try block reads the state without attempting to modify it.


lock, it sets lockTaken to true. The finally block examines lockTaken to know whether to call

Monitor.Exit or not. By the way, the SpinLock structure also supports this lockTaken pattern.

 

 


Date: 2016-03-03; view: 864


<== previous page | next page ==>
Nbsp;   Spinning, Thread Ownership, and Recursion | TheReaderWriterLockSlim Class
doclecture.net - lectures - 2014-2024 year. Copyright infringement or personal data (0.007 sec.)