Chapter 13. Threading and Synchronization

Although Java has been a threaded language from inception, support for multithreaded programming in the base language has benefited from little innovation in recent years. Furthermore, while significant achievements have been made outside the base language—for example, Concurrent Programming in Java, by Doug Lea—the base language itself still supports only the lowest available common denominator because of the platform portability requirements of Java Byte-Code.

By contrast, Microsoft .NET and the common language runtime (CLR) expose many of the rich threading features available in Microsoft Windows and provide fine-grain control over the thread life cycle. .NET applications can also benefit from the many lessons learned in developing robust multithreaded Java applications, including the techniques discussed in Doug Lea’s innovative concurrent programming library, available at www.gee.cs.oswego.edu/dl/cpj.

With threading, richness equates to complexity, and complexity leads to problems that are difficult to track down and fix. The first part of this chapter will cover the areas where .NET and Java overlap, highlighting the more complex and dangerous features. The second part of this chapter explores the ThreadPool class, which is significantly different from the Java ThreadGroup class. In .NET, this class provides access to a pool of threads that will take work items from a queue, and it can be used to simplify threading for some applications. The remainder of the chapter is given over to synchronization. As with threading, there is commonality between .NET and Java, but closer inspection reveals that .NET provides a more sophisticated and complete approach.

Threads

This section discusses the .NET support for threading and illustrates the differences between the Java and .NET approaches.

Creating and Starting Threads

In Java, threading operations are centered on the java.lang.Thread class. .NET has the equivalent System.Threading.Thread class. Unlike Java, .NET doesn’t allow classes to be derived from Thread, so creating a thread is more like the use of the Java Runnable interface, where the code to be executed is contained in another class. In .NET, threads are created by passing an instance of the ThreadStart delegate to the constructor of a new Thread instance. Here’s a simple threading example in Java using the Runnable interface:

public class Example implements Runnable {

    public void run() {
        for (int i = 0; i < 10; i++) {
            System.out.println("Counter: " + i);
        }
    }

    public Example() throws Exception {
        Thread x_thread = new Thread(this);
        x_thread.start();
    }
}

The Example class implements the Runnable interface, which is passed as a parameter to a new instance of Thread. When the Thread is started, it calls the Example.run method.

Here’s the equivalent in C#:

using System;
using System.Threading;

namespace Example {

    class Example {

        public void run() {
            for (int i = 0; i < 10; i++) {
                Console.WriteLine("Counter " + i);
            }
        }

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

Key points worth highlighting include the following:

  • The System.Threading namespace must be imported into each class file that uses the threading classes.

  • The ThreadStart delegate will accept any method that has no arguments and returns type void.

  • The method that is passed to the delegate doesn’t have to be called run and doesn’t need to be public.

  • .NET doesn’t define a functional equivalent of the ThreadGroup class in Java.

Suspending and Resuming Threads

Another difference between .NET and Java is that the methods suspend and resume are available for use in a C# application. In Java, these methods (and the stop method) have been deprecated because they are considered inherently unsafe. While the same potential problems with the Suspend and Resume methods exist in a .NET application, Microsoft decided not to eliminate this functionality but rather to allow developers to take their own precautions.

It’s recommended that Java threads be controlled by modifying a variable that is periodically checked during execution, a process known as polling. This is commonly achieved by setting the thread reference to null and checking that the reference matches the current thread during iteration:

private volatile Thread o_blinker;

public void stop() {
    o_blinker = null;
}

public void run() {
    Thread x_this_thread = Thread.currentThread();
    while (o_blinker == x_this_thread) {
        // perform some operations
    }
}

Besides reducing flexibility and introducing complexity, this approach has one major consequence: for methods that take a long time to complete, the responsiveness of the thread is dependent on the frequency with which the thread reference is checked. The programmer has to decide between checking the state frequently and accepting that the call to stop might not result in the thread being halted immediately.

By contrast, .NET supports almost immediate thread control using the Suspend and Resume methods. These methods work as their names indicate. Resuming a thread that has been suspended causes execution to continue from the point at which it stopped. Watch out for the exceptions that can be thrown; it’s easy to forget about them since C# doesn’t enforce declaring exceptions on method signatures.

Both methods will throw a ThreadStateException if the thread hasn’t been started or because the thread is dead. In addition, Resume will throw this exception if the thread hasn’t been suspended. Checking the thread state before making one of these calls can help minimize these exceptions. See the Thread States section later in this chapter.

Also note that these calls will throw a SecurityException if the caller doesn’t have the required permission. See Chapter 17, for more information on security.

Stopping Threads

The .NET Thread class provides an overloaded method, named Abort, for stopping the execution of a thread. Calling this method causes a ThreadAbort-Exception to be raised in the method that was passed to the ThreadStart instance when the thread was instantiated.

There are two overloads on this method:

public void Abort();
public void Abort(object);

The first version takes no parameters, causes the exception to be thrown, and begins the process of terminating the thread. The second overload takes an object as an argument, which is made available through the exception, and can be used to tidy up.

It’s possible to catch a ThreadAbortException, but this doesn’t prevent the thread from being killed. The exception will be thrown again at the end of the try...catch block, but only after any finally blocks have been executed. This presents an opportunity to clean up any incomplete state and to ensure that any resources are correctly released.

The following fragment shows how this may be done:

public void run() {
    try {
        while (true) {
            try {
                // do some important operation that can result
                // in state problems if stopped suddenly
            } catch (ThreadAbortException) {
                Console.WriteLine("Got inner abort exception.");
            } finally {
                Console.WriteLine("Tidy up.");
            }
        }
    } catch (ThreadAbortException) {
        Console.WriteLine("Got outer abort exception.");
    }
}

The results of passing a method into a ThreadStart delegate and then calling Abort on the running thread are shown below:

Got inner abort exception.
Tidy up.
Got outer abort exception.

The opportunity to tidy up occurs at each point in the code where the ThreadAbortException is caught. If the exception is not caught, it will unwind up the stack to the thread class instance and will not be seen again. The try...catch...finally block is not required since there is no state to maintain and notifications about thread deaths are not required.

Although calling Abort seems neater and more intuitive than the Java approach detailed earlier, there is no way to determine where the exception will be thrown. It falls to the developer to ensure that no unusual state is left behind if, for example, the exception is thrown during iteration.

Although calls to Abort usually result in threads being stopped, the CLR makes no guarantees. An unbounded computation (one that will never complete) can be started in the catch block for ThreadAbortException. For this reason, it’s important to use the catch block only as intended and not to perform further calculations. If the caller of the Abort method needs to ensure that the thread has stopped, the Join method can be used, which will block until the thread has been unraveled and stopped.

Another reason that a thread might not stop is that Thread.ResetAbort might be called in the catch statement. This doesn’t stop the exception from being thrown again but does prevent the thread from terminating. This method should be used with care because any other threads blocking through the Join method won’t return as expected. Time should be taken to consider why a thread should refuse to die when asked.

Caution

We can understand why the ResetAbort method has been added, but we feel that it’s too dangerous for general use and recommend that you use it as little as possible.

We have seen the Abort and ResetAbort methods used to try to build a cleaner version of the Java approach to stopping threads, by which an internal flag is used to cause the thread to stop naturally after the current iteration. This approach is problematic because it’s impossible to continue the current iteration correctly without knowing where the exception is thrown. If it’s important that a thread finish the current iteration before stopping, we recommend using the Java approach, although other threads might still call the Abort method and cause unexpected results.

Finally, if a thread is blocking because a call has been made to Wait, the ThreadAbortException won’t be raised until the thread returns from the call or is interrupted, meaning that the target thread won’t be stopped immediately. It’s possible to determine in advance whether this is likely to be the case by examining the state of the thread, which is discussed later in this chapter in the Thread States section.

Setting Thread Priorities

Thread priorities in .NET are handled in much the same way as in Java. The .NET Thread class has a property named Priority, which controls the thread’s priority level using the enumeration System.Threading.ThreadPriority. Five priority levels are defined in .NET, vs. 10 in Java, and they are (in descending priority): Highest, AboveNormal, Normal, BelowNormal, and Lowest. By default, threads are created with Normal priority.

As with Java, the scheduling of threads is driven by the underlying operating system. The operating system is not required to honor thread priorities.

There is no direct equivalent of the Java yield method in .NET. To ensure that threads of equal priority execute in a .NET application, use Thread.Sleep(0).

Thread States

.NET threads have a defined set of states that demarcate their life cycle. These states are defined in the System.Threading.ThreadState enumeration and are described in Table 13-1.

Table 13-1. The Thread.ThreadState Enumeration

Thread State

Description

Aborted

The thread is in the Stopped state.

AbortRequested

The Abort method has been called for this thread, but the ThreadAbort-Exception hasn’t been dispatched yet.

Background

The thread is running as a background thread. This is similar to the Java daemon thread.

Running

The thread has been started. It isn’t blocked, and no call has been made to Abort.

Stopped

The thread has stopped.

StopRequested

This is for internal use only.

Suspended

The thread has been suspended via the Suspend method.

SuspendRequested

The thread is being requested to suspend.

Unstarted

The thread has been created but not started via the Start method.

WaitSleepJoin

The thread is blocked because of a call to one of the following methods: Wait, Sleep, or Join.

These states are accessible through the read-only Thread.ThreadState property. Threads can be in more than one state, so it’s important to check the state carefully. For example, if a thread is blocked because of a call to the Monitor.Wait class and another thread calls Abort, the blocked thread will be in both the WaitSleepJoin and AbortRequested states. (See the Synchronization section later in this chapter for an explanation of the Monitor.Wait class.) When the blocked thread returns from the Wait call or is interrupted, the state will be just AbortRequested and the ThreadAbortException will be thrown, leading to an Aborted state.

Key points worth highlighting include the following:

  • The IsAlive property returns true if the thread state doesn’t include Unstarted, Stopped, and Aborted.

  • The IsBackground property returns true if the thread is running in the background. This is similar to the Java isDaemon method. Background threads are identical to normal threads except that they don’t prevent a process from terminating. If a process has no active foreground threads, Abort calls will be dispatched to any running background threads and the process will then terminate.

Table 13-2 lists actions and the effect they have on thread state.

Table 13-2. Effect of Actions on Thread State

Action Performed

Thread State Becomes...

A new thread is started.

Unstarted

A thread calls Start.

Running

The thread begins running.

Running

The thread calls Sleep.

WaitSleepJoin

The thread calls Wait on another Object.

WaitSleepJoin

Another thread calls Interrupt.

Running

Another thread calls Suspend.

SuspendRequested

The thread is suspended.

Suspended

Another thread calls Resume.

Running

Another thread calls Abort.

AbortRequested

The thread responds to an abort request.

Stopped

A thread is terminated.

Stopped

When using the thread states, bear in mind that they can change quickly and might be altered before subsequent calls are made. Just because the thread has a state of Suspended when checked doesn’t necessarily mean that it will remain that way for long before another thread calls Resume. For this reason, it’s important to check for the possible exceptions when using the thread calls.

Interrupting a Thread

Both Java and .NET provide similar means to interrupt a thread that is blocking. In .NET, a thread is blocked when it’s sleeping (via the Sleep method), waiting for another thread to end (through the Join method), waiting to obtain access to a synchronized code block, or waiting to be signaled by an event.

There are two significant differences when interrupting a thread with .NET. The first is that since C# doesn’t require exceptions to be declared in the way that Java does, it’s easy to miss instances of ThreadInterruptedException—the .NET equivalent of the Java InterruptedException—being thrown. These exceptions roll up the call stack of the target thread and can disappear without a trace. If threading code shouldn’t terminate when the thread is interrupted, it’s vital that these exceptions be caught.

The second difference is more dangerous. With Java, calling Interrupt on a thread that isn’t blocking has no effect. In .NET, however, calling Interrupt on a thread that isn’t blocking will cause the thread to be interrupted the next time it blocks. This means that calls to interrupt a thread can have unexpected results long after Interrupt is called, creating a problem that can be difficult to track down. We advise caution in the use of this method in .NET.

Local Thread Data

Since version 1.2, Java has provided the ThreadLocal class, representing a specialized variable type in which each thread that sets or gets the value modifies its own independent copy of the variable. .NET makes some broadly similar features available, but in contrast with Java, the calls that drive these features are integrated directly into the Thread class and are based on the idea of data slots.

There is also a metadata annotation using attributes that allows for a similar effect without directly using the Thread class.

Data Slots

Dealing with data slots is a two-stage process, first allocating the slot (akin to creating a new instance of ThreadLocal in Java) and then getting or setting the associated data (just like the get and set methods in ThreadLocal).

There are two approaches to allocating slots that affect access to them and the way in which they are garbage collected. The first kind, known as an unnamed slot, is most like Java in that to access the slot a thread must have a handle on the reference to the slot itself (an instance of the System.LocalDataStoreSlot type). If no threads are referencing the slot, it will be garbage collected automatically.

The second approach is to create a named slot, which is available to any thread that knows the name assigned during construction. Named slots aren’t automatically garbage collected when they fall out of use and must be explicitly freed using the FreeNamedDataSlot method.

Accessing the data is the same for both approaches since the result of the allocate operations is the same data type, and this is the argument that is passed into the accessor methods in the Thread class.

Here’s a code fragment that illustrates how to create the two kinds of slot:

LocalDataStoreSlot x_named_slot =
    Thread.AllocateNamedDataSlot("my slot");
LocalDataStoreSlot x_unnamed_slot = Thread.AllocateDataSlot();

Both calls return the same type. The method to set the variable data is the same for both types of slot:

Thread.SetData(x_named_slot, "data");
Thread.SetData(x_unnamed_slot, "data");

The method to get the variable data is the same, but with the named slot it’s possible to get a reference to the LocalDataStoreSlot instance by knowing the name:

// get the data
object x_unnamed_data = Thread.GetData(x_unnamed_slot);
object x_named_data =
    Thread.GetData(Thread.GetNamedDataSlot("my slot"));

To release a named slot, call Thread.FreeNamedDataSlot, passing in the name of the slot as the only argument.

Unlike Java, .NET doesn’t provide a means for child threads to inherit the slot values from the parent, provided in Java via the InheritableThreadLocal class.

Metadata Annotation

The extensive .NET support for metadata can also be used to achieve local thread data in a way that is more familiar in implementation to Java programmers. Static variables annotated with the ThreadStatic attribute aren’t shared among threads. Each thread has a separate instance of the variable, and modifications affect a copy local to the thread.

An annotated class variable looks like this:

[ThreadStatic] static int S_COUNTER;

Once annotated, the variable can be used as normal. It’s important to ensure that the static keyword is used. An annotated instance variable acts as an ordinary variable and will be shared between threads.

Timers

Many threads sleep for most of the time, waking periodically to perform some system or background task—for example, clearing a cache or updating a window. Creating threads consumes system resources, so a common approach to addressing this issue is to provide timers that will signal a callback after a given time period to perform the background task. The advantage of this approach is that the operating system or runtime platform can optimize the way that these signals are managed and use fewer resources to attain the same effect as a pool of programmer-created threads.

.NET provides three different timers to programmers: System.Threading.Timer, System.Timers.Timer, and System.Windows.Forms.Timer. Broadly speaking, the Forms.Timer class should be used only for UI applications, as its precision is limited to around 55 ms; for high-performance applications, this level of granularity can be a problem.

The System.Threading.Timer class uses the ThreadPool class, discussed in The ThreadPool Class section later in this chapter, to periodically call delegates. This approach is more accurate than the Forms.Timer approach and is suitable for general use.

The System.Timers.Timer class provides the greatest accuracy and flexibility. Time ticks are generated by the operating system and place the lowest demand on system resources.

We’ll ignore the Forms.Timer class—also known as the Windows timer—and focus on the more accurate implementations, which are useful in server-side programming.

System.Threading.Timer

The System.Threading.Timer class is simple to use and relies on a state object and a TimerCallback delegate to signal. The state object is optional. Here’s an example that uses a timer to print a message:

class ThreadTimer {
    Timer o_timer;

    public ThreadTimer() {
        // create the timer
        o_timer = new Timer(new TimerCallback(TimerCallbackHandler),
            null, 1000, 2000);
    }

    void TimerCallbackHandler(object p_state) {
        DateTime x_now = DateTime.Now;
        Console.WriteLine("Timer Called: {0}:{1}:{2} ", x_now.Hour,
            x_now.Minute, x_now.Second);
    }
}

In this example, we create a Timer, specifying a new TimerCallback, a delegate that requires a method with a void return and a single object parameter. The first numeric argument specifies the number of milliseconds before the timer first signals the callback, and the second argument specifies how frequently the timer should subsequently signal the callback, again in milliseconds. The null argument represents a state object that’s passed into the callback method; we don’t use this feature in the example.

As soon as the timer is created, it will begin waiting for the initial delay to elapse. We keep an instance reference to the timer in the class that created it because the timer will continue to signal the delegate until it is disposed of. If we do not have a reference, we cannot make that call. A timer is terminated by calling the Dispose method.

Another reason that we kept a reference to the Timer instance is that it’s possible to change the details of the timer after it has been created, in effect restarting the timer. This can be done with the Change method, which takes two long arguments. The first argument specifies how many milliseconds should pass before the delegate is signaled again, and the second argument specifies how many milliseconds should elapse between subsequent signals.

The output from this example will be something similar to the following:

Timer Called: 14:23:10
Timer Called: 14:23:12
Timer Called: 14:23:14
Timer Called: 14:23:16

System.Timers.Timer

The System.Timers.Timer class is more sophisticated than the other timers but is as easy to use. Here’s a version of the preceding example using this timer:

class TimerExample {

    public TimerExample() {
        System.Timers.Timer x_timer = new System.Timers.Timer(2000);
        x_timer.Elapsed += new ElapsedEventHandler(onElapsed);
        x_timer.AutoReset = true;
        x_timer.Enabled = true;
    }

    public static void onElapsed(
        object p_source,
        ElapsedEventArgs p_event) {

        DateTime x_now = DateTime.Now;
        Console.WriteLine("Timer Called: {0}:{1}:{2} ",
            x_now.Hour, x_now.Minute, x_now.Second);
    }
}

Although this timer is more flexible than the System.Threading.Timer class, it doesn’t have support for delaying the initial signal to the handler. However, the timer doesn’t start automatically; and it can be started, stopped, and restarted using the Enabled property.

An event is raised each time the timer fires. The signature for the event handler requires a source object (which is the timer that invoked the event) and an event argument. The source reference can be used to control the timer without needing a reference to be kept as with the first example. The ElapsedEventArgs argument contains the SignalTime property, which indicates the time at which the signal was raised—this can differ from when the signal is received.

The additional flexibility available when working with a System.Timers.Timer comes from the way that these timers can be stopped and started and also from their AutoReset property, which allows an instance of this timer to represent an event that should be repeated or should no longer be fired. Because the timer instance is passed into the event handler, this can be controlled on the fly. In addition, being aware of which timer has raised a signal is better suited to complex code, which can maintain a number of timers. Since this class raises signals via events, multiple handlers can be attached to a single timer, although we advise caution if attempting to modify the timer settings since the other listeners may then produce unexpected results.

This is the timer class that we tend to use when defining systemwide events, such as the start of a batch process or rolling log files. By having all actions that should occur together tied to a single timer, we can vary the period between actions in a single place and have the effect propagate through the code seamlessly. For other tasks, we favor System.Threading.Timer.

Basic Threading Summary

Table 13-3 summarizes the basic threading members for Java and .NET.

Table 13-3. Comparison Between the Basic Threading Members for Java and .NET

Java

.NET

Comments

Runnable

ThreadStart

Delegate.

Thread.currentThread

Thread.CurrentThread

Property.

Thread.getName

Thread.Name

Property.

Thread.getPriority

Thread.setPriority

Property. .NET uses an enumeration to define thread states.

Thread.Priority

  

Thread.getThreadGroup

N/A

 

Thread.interrupt

Thread.Interrupt

Method.

Thread.interrupted

N/A

 

Thread.isInterrupted

N/A

 

Thread.isDaemon

Thread.IsBackGround

Property.

N/A

Thread.IsAlive

Property.

N/A

Thread.IsThreadPoolThread

Property.

Thread.join

Thread.Join

Method.

Thread.suspend (dep)

Thread.Suspend

Method.

Thread.resume (dep)

Thread.Resume

Method.

Thread.run

N/A

.NET uses the ThreadStart delegate to define threads.

Thread.sleep

Thread.Sleep

Method.

Thread.start

Thread.Start

Method.

Thread.stop

Thread.Abort

Method.

N/A

Thread.ResetAbort

Method.

N/A

Thread.SpinWait

Method.

Thread.yield

Thread.Sleep(0)

Method.

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

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