Monitor objects expose the ability to synchronize access to a region of code by taking and releasing a lock on a particular object using the Monitor::Enter, Monitor::TryEnter, and Monitor::Exit methods. Once you have a lock on a code region, you can use the Monitor::Wait, Monitor::Pulse, and Monitor::PulseAll methods. Wait releases the lock if it is held and waits to be notified. When Wait is notified, it returns and obtains the lock again. Both Pulse and PulseAll signal for the next thread in the wait queue to proceed.
Typically, you enter the monitor by calling the Monitor::Enter or Monitor::TryEnter method immediately before executing the code in a try block. You then exit the monitor by calling the Monitor::Exit method in a finally block. This ensures that the lock is released even if an exception occurs.
The Visual Basic SyncLock and C# lock statements use Monitor::Enter to take the lock and Monitor::Exit to release it. The advantage of using the language statements is that everything in the lock or SyncLock block is included in a Try statement. The Try statement has a Finally block to guarantee that the lock is released.
Monitor locks objects (that is, reference types), not value types. While you can pass a value type to Enter and Exit, it is boxed separately for each call. Since each call creates a separate object, Enter never blocks, and the code it is supposedly protecting is not really synchronized. In addition, the object passed to Exit is different from the object passed to Enter, so Monitor throws SynchronizationLockException exception with the message "Object synchronization method was called from an unsynchronized block of code."
The following example illustrates this problem. It launches ten tasks, each of which just sleeps for 250 milliseconds. Each task then updates a counter variable, nTasks, which is intended to count the number of tasks that actually launched and executed. Because nTasks is a global variable that can be updated by multiple tasks simultaneously, a monitor is used to protect it from simultaneous modification by multiple tasks. However, as the output from the example shows, each of the tasks throws a SynchronizationLockException exception.
Each task throws a SynchronizationLockException exception because the nTasks variable is boxed before the call to the Monitor::Enter method in each task. In other words, each method call is passed a separate variable that is independent of the others. nTasks is boxed again in the call to the Monitor::Exit method. Once again, this creates ten new boxed variables, which are independent of each other, nTasks, and the ten boxed variables created in the call to the Monitor::Enter method. The exception is thrown, then, because our code is attempting to release a lock on a newly created variable that was not previously locked.
Although you can box a value type variable before calling Enter and Exit, as shown in the following example, and pass the same boxed object to both methods, there is no advantage to doing this. Changes to the unboxed variable are not reflected in the boxed copy, and there is no way to change the value of the boxed copy.
It is important to note the distinction between the use of the Monitor and WaitHandle objects. Monitor objects are purely managed, fully portable, and might be more efficient in terms of operating-system resource requirements. WaitHandle objects represent operating-system waitable objects, are useful for synchronizing between managed and unmanaged code, and expose some advanced operating-system features like the ability to wait on many objects at once.
The following example demonstrates the combined use of the Monitor class (implemented with the lock and SyncLock compiler statements), the Interlocked class, and the AutoResetEvent class. It defines two internal (in C#) or Friend (in Visual Basic) classes, SyncResource and UnSyncResource, that respectively provide synchronized and unsynchronized access to a resource. To ensure that the example illustrates the difference between the synchronized and unsynchronized access (which could be the case if each method call completes rapidly), the method includes a random delay: for threads whose Thread::ManagedThreadId property is even, the method calls Thread::Sleep to introduce a delay of 2,000 milliseconds. Note that, because the SyncResource class is not public, none of the client code takes a lock on the synchronized resource; the internal class itself takes the lock. This prevents malicious code from taking a lock on a public object.
The example defines a variable, numOps, that defines the number of threads that will attempt to access the resource. The application thread calls the ThreadPool::QueueUserWorkItem(WaitCallback^) method for synchronized and unsynchronized access five times each. The ThreadPool::QueueUserWorkItem(WaitCallback^) method has a single parameter, a delegate that accepts no parameters and returns no value. For synchronized access, it invokes the SyncUpdateResource method; for unsynchronized access, it invokes the UnSyncUpdateResource method. After each set of method calls, the application thread calls the AutoResetEvent.WaitOne method so that it blocks until the AutoResetEvent instance is signaled.
Each call to the SyncUpdateResource method calls the internal SyncResource.Access method and then calls the Interlocked::Decrement method to decrement the numOps counter. The Interlocked::Decrement method Is used to decrement, because otherwise you cannot be certain that a second thread will access the value before a first thread's decremented value has been stored in the variable. When the last synchronized worker thread decrements the counter to zero, indicating that all synchronized threads have completed accessing the resource, the SyncUpdateResource method calls the EventWaitHandle.Set method, which signals the main thread to continue execution.
Each call to the UnSyncUpdateResource method calls the internal UnSyncResource.Access method and then calls the Interlocked::Decrement method to decrement the numOps counter. Once again, the Interlocked::Decrement method Is used to decrement to ensure that a second thread does not access the value before a first thread's decremented value has been assigned to the variable. When the last unsynchronized worker thread decrements the counter to zero, indicating that no more unsynchronized threads need to access the resource, the UnSyncUpdateResource method calls the EventWaitHandle.Set method, which signals the main thread to continue execution.
As the output from the example shows, synchronized access ensures that the calling thread exits the protected resource before another thread can access it; each thread waits on its predecessor. On the other hand, without the lock, the UnSyncResource.Access method is called in the order in which threads reach it.