Synchronization

The basic synchronization support in .NET is similar to that in Java. For advanced functionality, .NET goes into areas not covered by Java, exposing some sophisticated tools and fine-grain controls.

Basic Locking

In Java, the keyword synchronized is used to ensure that only one thread will enter a specific section of code at a time. In the following example, Java ensures that a thread will not enter the code block containing the comment until an exclusive lock has been obtained on the this object.

public void doSomething() {
    synchronized (this) {
        // some synchronized operation
    }
}

When the thread exits the code section, Java will automatically release the lock so that another thread can attempt to acquire it and enter the synchronized code block.

C# provides the keyword lock, which does the same thing. The Java fragment above is translated to C# as follows:

public void doSomething() {
    lock (this) {
        // some synchronized operation
    }
}

C# doesn’t provide a direct analog of using the Java synchronized keyword as a method modifier, but the same effect can be achieved by annotating the method with the MethodImpl(MethodImplOptions.Synchronized) attribute.

So a synchronized method in Java:

private synchronized void doLock() {
    // do something
}

looks like this in C#:

[MethodImpl(MethodImplOptions.Synchronized)]
private void doLock() {
    // do something
}

The MethodImpl attribute is defined in the System.Runtime.CompilerServices namespace, so this must be imported before the method can be annotated. Key points worth highlighting include the following:

  • No implicit boxing is performed on the lock object, so value types, such as an integer or a floating-point number, cannot be used to perform locking. Attempting to use a value type will result in a compile-time error.

  • To protect a static method or variable, the use of this is not available, but it’s possible to use the return from typeof(MyClass).

Waiting and Notifying

In Java, the Object class contains the methods wait, notify, and notifyAll. The .NET Object class doesn’t contain the equivalent methods, but the System.Threading.Monitor class exposes the required functionality, as detailed in Table 13-4.

Table 13-4. Mapping the Java Object Class Threading Methods to .NET

Java

.NET

Object.wait()

Monitor.Wait(object)

Object.notify()

Monitor.Pulse(object)

Object.notifyAll()

Monitor.PulseAll(object)

The only significant difference is that the Monitor class requires the programmer to pass in an object on which to acquire a lock. Like Java, C# requires these calls to be performed within a synchronized section of code, which is accomplished using the lock keyword.

The Monitor class also provides some functionality that Java doesn’t have, giving the programmer more control over how locks are acquired and released.

Enter, TryEnter, and Exit

The Enter and Exit methods in the .NET Monitor class provide the same basic support as the lock keyword, albeit with additional flexibility. Locks acquired with the Enter call can be released in different methods (or classes) by calling the Exit method.

A call to Monitor.Enter will cause .NET to either acquire a lock on the Object passed in as the argument or block until the lock can be acquired.

When blocking, the thread is in the WaitSleepJoin thread state and can be interrupted by another thread calling Interrupt. This will cause the thread to unblock and will throw a ThreadIntrerruptedException.

Once acquired, a lock can be released by calling Monitor.Exit. A thread can request the lock for an object repeatedly without blocking, but Microsoft recommends that Exit be called the same number of times as Enter. Calling Exit to release a lock that isn’t held by the current thread will result in an instance of SynchronizationLockException being thrown.

The Monitor class also provides the ability to attempt to acquire a lock on an object without blocking, akin to checking to see whether a lock is available, acquiring if available and returning if not.

This feature is made available through the TryEnter method. If only an object is supplied as an argument, an attempt will be made to acquire the lock for the object and the success of the attempt is returned as a bool. If an int is supplied as well as the object, the CLR will try to acquire the lock for the specified number of milliseconds before returning.

ReaderWriterLock

Although most of the synchronization support in .NET works based on acquiring a lock on a target Object, a different model is available in the ReaderWriterLock class. This class maintains two levels of lock, an exclusive lock for writers and a nonexclusive one for readers. Many readers are allowed to acquire the read lock simultaneously, but only if the write lock isn’t held. One writer can acquire the write lock, but only if there are no active readers. Readers can upgrade to get a handle on the write lock.

We won’t cover this class in detail because its use is simple and obvious. This class is ideal for implementing custom collections.

Note

Requests for locks are handled on a first come–first served basis. This means that no more readers will be let through once a writer has requested a lock.

The Java language specification guarantees that any operation on a 32-bit or smaller value will be atomic, without the need to explicitly synchronize access to the variable. With its richer set of data types, .NET takes a different approach, allowing the developer to use the Interlocked class to implement atomic access to variables. Interlocked allows int and long types to be exchanged, incremented, and decremented in a thread-safe manner, with the same effect as wrapping the operations in a lock block. For example:

private void incrementCounter() {
    lock (this) {
        o_counter++;
    }
}

This fragment can be rewritten to use Interlocked as follows:

private void incrementCounter() {
    Interlocked.Increment(ref o_counter);
}

Using the Interlocked class introduces tidier code and removes the need to use typeof to protect static variables. The variable must be annotated with the ref keyword to ensure that the target variable is modified. Omitting the annotation causes the value of the instance to be copied and modified without affecting the original instance, in which case simultaneous calls to Interlocked will be dealing with different values.

WaitHandle

The WaitHandle class is used as a base class for synchronization classes that interoperate with the native operating system. The class defines a signaling mechanism that is used to indicate that a shared resource has been acquired or released. The significance of this class comes from the static methods that allow callers to block until one or more instances of WaitHandle are signaled. Three methods are available:

  • WaitOne. Waits for a single WaitHandle to be signaled.

  • WaitAny. Static method; waits for any one of an array of WaitHandle objects to be signaled.

  • WaitAll. Static method; waits for all of the WaitHandles in an array to be signaled.

The three classes in the namespace that are derived from WaitHandle are discussed in the following sections.

Mutex

The Mutex class can be used to provide synchronization between processes. Here’s a simple example of using the Mutex class:

Mutex x_mutex = new Mutex(false, "shared");
Console.WriteLine("Created mutex");

while (true) {
    string x_str = Console.ReadLine();
    if (x_str.Equals("a")) {
        Console.WriteLine("Attempting to acquire...");
        x_mutex.WaitOne();
        Console.WriteLine("Acquired the mutex");
    } else if (x_str.Equals("r")) {
        Console.WriteLine("Releasing");
        x_mutex.ReleaseMutex();
        Console.WriteLine("Released");
    }
}

Running two instances of this code on a machine demonstrates a shared Mutex. Typing a causes the class to attempt to acquire the lock. Typing r will release the lock. If a process dies when holding the lock, the resource becomes available for acquisition.

The example illustrates a named Mutex. Processes that know the name of the Mutex can share named instances. Instances can also be created without names. With an unnamed Mutex, an object reference must be passed to enable another thread or process to synchronize against it.

Tip

Any process that knows the name of the instance can share a named Mutex. Choose names that are project specific. Be aware that other programmers may attempt to acquire the lock for reasons that were not intended, leading to deadlocks.

AutoResetEvent and ManualResetEvent

The final classes that we’ll cover in this chapter are AutoResetEvent and ManualResetEvent. Threads calling the waitOne method of these classes will block until the event is signaled through a call to the Set method.

The AutoResetEvent is constructed with a single argument that dictates the initial signal state. Using true as the argument will result in the first call to waitOne not blocking the thread. This class provides a simpler approach to using the Monitor class in order to coordinate access to shared resources driven by external events.

In the following example, once started the thread will block on the waitOne method until the user types the s key, when the default thread will set the event signal. This releases the thread, which writes a message to the console. The AutoResetEvent resets automatically—hence the name—and subsequent calls to waitOne will also block:

class AutoEvent {
    AutoResetEvent o_event = new AutoResetEvent(false);

    public AutoEvent() {
        Thread x_thread = new Thread(new ThreadStart(run));
        x_thread.Start();

        while (true) {
            if (Console.Read() == 's') {
                o_event.Set();
            }
        }
    }

    public void run() {
        while (true) {
            o_event.WaitOne();
            Console.WriteLine("Event signalled");
        }
    }
}

There is also a ManualResetEvent class that remains signaled after the Set method is used (meaning that calls to waitOne will not block). To reset the signal state, the Reset method must be used, causing waitOne calls to block until the event is signaled again.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset