
Pro CSharp And The .NET 2.0 Platform (2005) [eng]
.pdf
464 C H A P T E R 1 4 ■ B U I L D I N G M U LT I T H R E A D E D A P P L I C AT I O N S
To begin, set a reference to the System.Windows.Forms.dll assembly and display a message within Main() using MessageBox.Show() (you’ll see the point of doing so once you run the program). Here is the complete implementation of Main():
static void Main(string[] args)
{
Console.WriteLine("***** The Amazing Thread App *****\n");
Console.Write("Do you want [1] or [2] threads? "); string threadCount = Console.ReadLine();
// Name the current thread.
Thread primaryThread = Thread.CurrentThread; primaryThread.Name = "Primary";
// Display Thread info.
Console.WriteLine("-> {0} is executing Main()", Thread.CurrentThread.Name);
// Make worker class.
Printer p = new Printer();
switch(threadCount)
{
case "2":
// Now make the thread.
Thread backgroundThread =
new Thread(new ThreadStart(p.PrintNumbers)); backgroundThread.Name = "Secondary"; backgroundThread.Start();
break; case "1":
p.PrintNumbers();
break;
default:
Console.WriteLine("I don't know what you want...you get 1 thread."); goto case "1";
}
// Do some additional work.
MessageBox.Show("I'm busy!", "Work on main thread..."); Console.ReadLine();
}
Now, if you run this program with a single thread, you will find that the final message box will not display the message until the entire sequence of numbers has printed to the console. As you are explicitly pausing for approximately two seconds after each number is printed, this will result in a less-than-stellar end user experience. However, if you select two threads, the message box displays instantly, given that a unique Thread object is responsible for printing out the numbers to the console (see Figure 14-7).

C H A P T E R 1 4 ■ B U I L D I N G M U LT I T H R E A D E D A P P L I C AT I O N S |
465 |
Figure 14-7. Multithreaded applications provide results in more responsive applications.
Before we move on, it is important to note that when you build multithreaded applications (which includes the use of asynchronous delegates) on single CPU machines, you do not end up with an application that runs any faster, as that is a function of a machine’s CPU. When running this application using either one or two threads, the numbers are still displaying at the same pace. In reality, multithreaded applications result in more responsive applications. To the end user, it may appear that the program is “faster,” but this is not the case. Threads have no power to make foreach loops execute quicker, to make paper print faster, or to force numbers to be added together at rocket speed. Multithreaded applications simply allow multiple threads to share the workload.
■Source Code The SimpleMultiThreadApp project is included under the Chapter 14 subdirectory.
Working with the ParameterizedThreadStart Delegate
Recall that the ThreadStart delegate can point only to methods that return void and take no arguments. While this may fit the bill in many cases, if you wish to pass data to the method executing on the secondary thread, you will need to make use of the ParameterizedThreadStart delegate type. To illustrate, let’s re-create the logic of the AsyncCallbackDelegate project created earlier in this chapter, this time making use of the ParameterizedThreadStart delegate type.
To begin, create a new console application named AddWithThreads and “use” the System.Threading namespace. Now, given that ParameterizedThreadStart can point to any method taking a System. Object parameter, you will create a custom type containing the numbers to be added:
class AddParams
{
public int a; public int b;
public AddParams(int numb1, int numb2)
{
a = numb1; b = numb2;
}
}
Next, create a static method in the Program class that will take an AddParams type and print out the summation of each value:

466 C H A P T E R 1 4 ■ B U I L D I N G M U LT I T H R E A D E D A P P L I C AT I O N S
public static void Add(object data)
{
if (data is AddParams)
{
Console.WriteLine("ID of thread in Main(): {0}", Thread.CurrentThread.GetHashCode());
AddParams ap = (AddParams)data; Console.WriteLine("{0} + {1} is {2}",
ap.a, ap.b, ap.a + ap.b);
}
}
The code within Main() is straightforward. Simply use ParameterizedThreadStart rather than
ThreadStart:
static void Main(string[] args)
{
Console.WriteLine("***** Adding with Thread objects *****");
Console.WriteLine("ID of thread in Main(): {0}", Thread.CurrentThread.GetHashCode());
AddParams ap = new AddParams(10, 10);
Thread t = new Thread(new ParameterizedThreadStart(Add)); t.Start(ap);
...
}
■Source Code The AddWithThreads project is included under the Chapter 14 subdirectory.
Foreground Threads and Background Threads
Now that you have seen how to programmatically create new threads of execution using the System. Threading namespace, let’s formalize the distinction between foreground threads and background threads:
•Foreground threads have the ability to prevent the current application from terminating. The CLR will not shut down an application (which is to say, unload the hosting AppDomain) until all foreground threads have ended.
•Background threads (sometimes called daemon threads) are viewed by the CLR as expendable paths of execution that can be ignored at any point in time (even if they are currently laboring over some unit of work). Thus, if all foreground threads have terminated, any and all background threads are automatically killed when the application domain unloads.
It is important to note that foreground and background threads are not synonymous with primary and worker threads. By default, every thread you create via the Thread.Start() method is automatically a foreground thread. Again, this means that the AppDomain will not unload until all threads of execution have completed their units of work. In most cases, this is exactly the behavior you require.
For the sake of argument, however, assume that you wish to invoke Printer.PrintNumbers() on a secondary thread that should behave as a background thread. Again, this means that the method pointed to by the Thread type (via the ThreadStart or ParameterizedThreadStart delegate) should be able to halt safely as soon as all foreground threads are done with their work. Configuring such a thread is as simple as setting the IsBackground property to true:

C H A P T E R 1 4 ■ B U I L D I N G M U LT I T H R E A D E D A P P L I C AT I O N S |
467 |
static void Main(string[] args)
{
Printer p = new Printer(); Thread bgroundThread =
new Thread(new ThreadStart(p.PrintNumbers)); bgroundThread.IsBackground = true; bgroundThread.Start();
}
Notice that this Main() method is not making a call to Console.ReadLine() to force the console to remain visible until you press the Enter key. Thus, when you run the application, it will shut down immediately because the Thread object has been configured as a background thread. Given that the Main() method triggers the creation of the primary foreground thread, as soon as the logic in Main() completes, the AppDomain unloads before the secondary thread is able to complete its work. However, if you comment out the line that sets the IsBackground property, you will find that each number prints to the console, as all foreground threads must finish their work before the AppDomain is unloaded from the hosting process.
For the most part, configuring a thread to run as a background type can be helpful when the worker thread in question is performing a noncritical task that is no longer needed when the main task of the program is finished.
■Source Code The BackgroundThread project is included under the Chapter 14 subdirectory.
The Issue of Concurrency
All the multithreaded sample applications you have written over the course of this chapter have been thread-safe, given that only a single Thread object was executing the method in question. While some of your applications may be this simplistic in nature, a good deal of your multithreaded applications may contain numerous secondary threads. Given that all threads in an AppDomain have concurrent access to the shared data of the application, imagine what might happen if multiple threads were accessing the same point of data. As the thread scheduler will force threads to suspend their work at random, what if thread A is kicked out of the way before it has fully completed its work? Thread B is now reading unstable data.
To illustrate the problem of concurrency, let’s build another C# console application named MultiThreadedPrinting. This application will once again make use of the Printer class created previously, but this time the PrintNumbers() method will force the current thread to pause for
a randomly generated amount of time:
public class Printer
{
public void PrintNumbers()
{
...
for (int i = 0; i < 10; i++)
{
Random r = new Random(); Thread.Sleep(1000 * r.Next(5)); Console.Write(i + ", ");
}
Console.WriteLine();
}
}


C H A P T E R 1 4 ■ B U I L D I N G M U LT I T H R E A D E D A P P L I C AT I O N S |
469 |
Now run the application a few more times. Figure 14-9 shows another possibility (your results will obviously differ).
Figure 14-9. Concurrency in action, take two
There are clearly some problems here. As each thread is telling the Printer to print out the numerical data, the thread scheduler is happily swapping threads in the background. The result is inconsistent output. What we need is a way to programmatically enforce synchronized access to the shared resources. As you would guess, the System.Threading namespace provides a number of synchronization-centric types. The C# programming language also provides a particular keyword for the very task of synchronizing shared data in multithreaded applications.
■Note If you are unable to generate unpredictable outputs, increase the number of threads from 10 to 100 (for example) or introduce a call to Thread.Sleep() within your program. Eventually, you will encounter the concurrency issue.
Synchronization Using the C# lock Keyword
The first technique you can use to synchronize access to shared resources is the C# lock keyword. This keyword allows you to define a scope of statements that must be synchronized between threads. By doing so, incoming threads cannot interrupt the current thread, preventing it from finishing its work. The lock keyword requires you to specify a token (an object reference) that must be acquired by a thread to enter within the lock scope. When you are attempting to lock down an instance-level method, you can simply pass in a reference to the current type:
// Use the current object as the thread token. lock(this)
{
// All code within this scope is thread-safe.
}
If you examine the PrintNumbers() method, you can see that the shared resource the threads are competing to gain access to is the console window. Therefore, if you scope all interactions with the Console type within a lock scope as so:


C H A P T E R 1 4 ■ B U I L D I N G M U LT I T H R E A D E D A P P L I C AT I O N S |
471 |
■Source Code The MultiThreadedPrinting application is included under the Chapter 14 subdirectory.
Synchronization Using the System.Threading.Monitor Type
The C# lock statement is really just a shorthand notation for working with the System.Threading.Monitor class type. Once processed by the C# compiler, a lock scope actually resolves to the following (which you can verify using ildasm.exe):
public void PrintNumbers()
{
Monitor.Enter(this); try
{
// Display Thread info.
Console.WriteLine("-> {0} is executing PrintNumbers()", Thread.CurrentThread.Name);
// Print out numbers. Console.Write("Your numbers: "); for (int i = 0; i < 10; i++)
{
Random r = new Random(); Thread.Sleep(1000 * r.Next(5)); Console.Write(i + ", ");
}
Console.WriteLine();
}
finally
{
Monitor.Exit(this);
}
}
First, notice that the Monitor.Enter() method is the ultimate recipient of the thread token you specified as the argument to the lock keyword. Next, all code within a lock scope is wrapped within a try block. The corresponding finally clause ensures that the thread token is released (via the Monitor.Exit() method), regardless of any possible runtime exception. If you were to modify the MultiThreadSharedData program to make direct use of the Monitor type (as just shown), you will find the output is identical.
Now, given that the lock keyword seems to require less code than making explicit use of the System.Threading.Monitor type, you may wonder about the benefits of using the Monitor type directly. The short answer is control. If you make use of the Monitor type, you are able to instruct the active thread to wait for some duration of time (via the Wait() method), inform waiting threads when the current thread is completed (via the Pulse() and PulseAll() methods), and so on.
As you would expect, in a great number of cases, the C# lock keyword will fit the bill. However, if you are interested in checking out additional members of the Monitor class, consult the .NET Framework 2.0 SDK documentation.
Synchronization Using the System.Threading.Interlocked Type
Although it always is hard to believe until you look at the underlying CIL code, assignments and simple arithmetic operations are not atomic. For this reason, the System.Threading namespace provides a type that allows you to operate on a single point of data atomically with less overhead than with the Monitor type. The Interlocked class type defines the static members shown in Table 14-4.

472 C H A P T E R 1 4 ■ B U I L D I N G M U LT I T H R E A D E D A P P L I C AT I O N S
Table 14-4. Members of the System.Threading.Interlocked Type
Member |
Meaning in Life |
CompareExchange() |
Safely tests two values for equality and, if equal, changes one of the |
|
values with a third |
Decrement() |
Safely decrements a value by 1 |
Exchange() |
Safely swaps two values |
Increment() |
Safely increments a value by 1 |
|
|
Although it might not seem like it from the onset, the process of atomically altering a single value is quite common in a multithreaded environment. Assume you have a method named AddOne() that increments an integer member variable named intVal. Rather than writing synchronization code such as the following:
public void AddOne()
{
lock(this)
{
intVal++;
}
}
you can simplify your code via the static Interlocked.Increment() method. Simply pass in the variable to increment by reference. Do note that the Increment() method not only adjusts the value of the incoming parameter, but also returns the new value:
public void AddOne()
{
int newVal = Interlocked.Increment(ref intVal);
}
In addition to Increment() and Decrement(), the Interlocked type allows you to atomically assign numerical and object data. For example, if you wish to assign the value of a member variable to the value 83, you can avoid the need to use an explicit lock statement (or explicit Monitor logic) and make use of the Interlocked.Exchange() method:
public void SafeAssignment()
{
Interlocked.Exchange(ref myInt, 83);
}
Finally, if you wish to test two values for equality to change the point of comparison in a threadsafe manner, you are able to leverage the Interlocked.CompareExchange() method as follows:
public void CompareAndExchange()
{
// If the value of i is currently 83, change i to 99.
Interlocked.CompareExchange(ref i, 99, 83);
}
Synchronization Using the [Synchronization] Attribute
The final synchronization primitive examined here is the [Synchronization] attribute, which is a member of the System.Runtime.Remoting.Contexts namespace. In essence, this class-level attribute effectively locks down all instance member code of the object for thread safety. When the CLR allocates objects attributed with [Synchronization], it will place the object within a synchronized

C H A P T E R 1 4 ■ B U I L D I N G M U LT I T H R E A D E D A P P L I C AT I O N S |
473 |
context. As you may recall from Chapter 13, objects that should not be removed from a contextual boundary should derive from ContextBoundObject. Therefore, if you wish to make the Printer class type thread-safe (without explicitly writing thread-safe code within the class members), you could update the definition as so:
using System.Runtime.Remoting.Contexts;
...
// All methods of Printer are now thread-safe!
[Synchronization]
public class Printer : ContextBoundObject
{
public void PrintNumbers()
{
...
}
}
In some ways, this approach can be seen as the lazy way to write thread-safe code, given that you are not required to dive into the details about which aspects of the type are truly manipulating thread-sensitive data. The major downfall of this approach, however, is that even if a given method is not making use of thread-sensitive data, the CLR will still lock invocations to the method. Obviously, this could degrade the overall functionality of the type, so use this technique with care.
At this point, you have seen a number of ways you are able to provide synchronized access to shared blocks of data. To be sure, additional types are available under the System.Threading namespace, which I will encourage you to explore at your leisure. To wrap up our examination of thread programming, allow me to introduce three additional types: TimerCallback, Timer, and ThreadPool.
Programming with Timer Callbacks
Many applications have the need to call a specific method during regular intervals of time. For example, you may have an application that needs to display the current time on a status bar via a given helper function. As another example, you may wish to have your application call a helper function every so often to perform noncritical background tasks such as checking for new e-mail messages. For situations such as these, you can use the System.Threading.Timer type in conjunction with a related delegate named TimerCallback.
To illustrate, assume you have a console application that will print the current time every second until the user presses a key to terminate the application. The first obvious step is to write the method that will be called by the Timer type:
class TimePrinter
{
static void PrintTime(object state)
{
Console.WriteLine("Time is: {0}", DateTime.Now.ToLongTimeString());
}
}
Notice how this method has a single parameter of type System.Object and returns void. This is not optional, given that the TimerCallback delegate can only call methods that match this signature. The value passed into the target of your TimerCallback delegate can be any bit of information whatsoever (in the case of the e-mail example, this parameter might represent the name of the Microsoft Exchange server to interact with during the process). Also note that given that this parameter is indeed a System.Object, you are able to pass in multiple arguments using a System.Array or custom class/structure.