
Pro CSharp And The .NET 2.0 Platform (2005) [eng]
.pdf
454 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 interface IAsyncResult
{
object AsyncState { get; } WaitHandle AsyncWaitHandle { get; } bool CompletedSynchronously { get; } bool IsCompleted { get; }
}
In the simplest case, you are able to avoid directly invoking these members. All you have to do is cache the IAsyncResult-compatible object returned by BeginInvoke() and pass it to EndInvoke() when you are ready to obtain the result of the method invocation. As you will see, you are able to invoke the members of an IAsyncResult-compatible object when you wish to become “more involved” with the process of fetching the method’s return value.
■Note If you asynchronously invoke a method that does not provide a return value, you can simply “fire and forget.” In such cases, you will never need to cache the IAsyncResult-compatible object or call EndInvoke() in the first place (as there is no return value to retrieve).
Invoking a Method Asynchronously
To instruct the BinaryOp delegate to invoke Add() asynchronously, you can update the previous Main() method as follows:
static void Main(string[] args)
{
Console.WriteLine("***** Async Delegate Invocation *****");
//Print out the ID of the executing thread.
Console.WriteLine("Main() invoked on thread {0}.", Thread.CurrentThread.GetHashCode());
//Invoke Add() on a secondary thread.
BinaryOp b = new BinaryOp(Add);
IAsyncResult iftAR = b.BeginInvoke(10, 10, null, null);
//Do other work on primary thread...
Console.WriteLine("Doing more work in Main()!");
//Obtain the result of the Add()
//method when ready.
int answer = b.EndInvoke(iftAR); Console.WriteLine("10 + 10 is {0}.", answer); Console.ReadLine();
}
If you run this application, you will find that two unique hash codes are displayed, given that there are in fact two threads working within the current AppDomain (see Figure 14-2).
In addition to the unique hash code values, you will also notice upon running the application that the Doing more work in Main()! message displays immediately, while the secondary thread is occupied attending to its business.

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 |
455 |
Figure 14-2. Methods invoked asynchronously are done so on a unique thread.
Synchronizing the Calling Thread
If you ponder the current implementation of Main(), you might have realized that the time span between calling BeginInvoke() and EndInvoke() is clearly less than five seconds. Therefore, once Doing more work in Main()! prints to the console, the calling thread is now blocked and waiting for the secondary thread to complete before being able to obtain the result of the Add() method. Therefore, you are effectively making yet another synchronous call:
static void Main(string[] args)
{
...
BinaryOp b = new BinaryOp(Add);
IAsyncResult iftAR = b.BeginInvoke(10, 10, null, null);
//This call takes far less than 5 seconds!
Console.WriteLine("Doing more work in Main()!");
//The calling thread is now blocked until
//EndInvoke() completes.
int answer = b.EndInvoke(iftAR);
...
}
Obviously, asynchronous delegates would lose their appeal if the calling thread had the potential of being blocked under various circumstances. To allow the calling thread to discover if the asynchronously invoked method has completed its work, the IAsyncResult interface provides the IsCompleted property. Using this member, the calling thread is able to determine if the asynchronous call has indeed completed before calling EndInvoke(). If the method has not completed, IsCompleted returns false, and the calling thread is free to carry on its work. If IsCompleted returns true, the calling thread is able to obtain the result in the “least blocking manner” possible. Ponder the following update to the Main() method:
static void Main(string[] args)
{
...
BinaryOp b = new BinaryOp(Add);
IAsyncResult iftAR = b.BeginInvoke(10, 10, null, null);
//This message will keep printing until
//the Add() method is finished. while(!iftAR.IsCompleted)
{
Console.WriteLine("Doing more work in Main()!");
}

456 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
// Now we know the Add() method is complete. int answer = b.EndInvoke(iftAR);
...
}
Here, you enter a loop that will continue processing the Console.WriteLine() statement until the secondary thread has completed. Once this has occurred, you can obtain the result of the Add() method knowing full well the method has indeed completed.
In addition to the IsCompleted property, the IAsyncResult interface provides the AsyncWaitHandle property for more flexible waiting logic. This property returns an instance of the WaitHandle type, which exposes a method named WaitOne(). The benefit of WaitHandle.WaitOne() is that you can specify the maximum wait time. If the specified amount of time is exceeded, WaitOne() returns false. Ponder the following updated while loop:
while (!iftAR.AsyncWaitHandle.WaitOne(2000, true))
{
Console.WriteLine("Doing more work in Main()!");
}
While these properties of IAsyncResult do provide a way to synchronize the calling thread, they are not the most efficient approach. In many ways, the IsCompleted property is much like a really annoying manager (or classmate) who is constantly asking, “Are you done yet?” Thankfully, delegates provide a number of additional (and more effective) techniques to obtain the result of a method that has been called asynchronously.
■Source Code The AsyncDelegate project is located under the Chapter 14 subdirectory.
The Role of the AsyncCallback Delegate
Rather than polling a delegate to determine if an asynchronously method has completed, it would be ideal to have the delegate inform the calling thread when the task is finished. When you wish to enable this behavior, you will need to supply an instance of the System.AsyncCallback delegate as a parameter to BeginInvoke(), which up until this point has been null. However, when you do supply an AsyncCallback object, the delegate will call the specified method automatically when the asynchronous call has completed.
Like any delegate, AsyncCallback can only invoke methods that match a specific pattern, which in this case is a method taking IAsyncResult as the sole parameter and returning nothing:
void MyAsyncCallbackMethod(IAsyncResult itfAR)
Assume you have another application making use of the BinaryOp delegate. This time, however, you will not poll the delegate to determine if the Add() method has completed. Rather, you will define a static method named AddComplete() to receive the notification that the asynchronous invocation is finished:
namespace AsyncCallbackDelegate
{
public delegate int BinaryOp(int x, int y);
class Program
{
static void Main(string[] args)
{
Console.WriteLine("***** AsyncCallbackDelegate Example *****");

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 |
457 |
Console.WriteLine("Main() invoked on thread {0}.",
Thread.CurrentThread.GetHashCode());
BinaryOp b = new BinaryOp(Add); IAsyncResult iftAR = b.BeginInvoke(10, 10,
new AsyncCallback(AddComplete), null);
// Other work performed here...
Console.ReadLine();
}
static void AddComplete(IAsyncResult itfAR)
{
Console.WriteLine("AddComplete() invoked on thread {0}.", Thread.CurrentThread.GetHashCode());
Console.WriteLine("Your addition is complete");
}
static int Add(int x, int y)
{
Console.WriteLine("Add() invoked on thread {0}.", Thread.CurrentThread.GetHashCode());
Thread.Sleep(5000); return x + y;
}
}
}
Again, the static AddComplete() method will be invoked by the AsyncCallback delegate when the Add() method has completed. If you run this program, you can confirm that the secondary thread is the thread invoking the AddComplete() callback (see Figure 14-3).
Figure 14-3. The AsyncCallback delegate in action
The Role of the AsyncResult Class
You may have noticed in the current example that the Main() method is not caching the IAsyncResult type returned from BeginInvoke() and is no longer calling EndInvoke(). The reason is that the target of the AsyncCallback delegate (AddComplete() in this example) does not have access to the original BinaryOp delegate created in the scope of Main(). While you could simply declare the BinaryOp variable as a static class member to allow both methods to access the same object, a more elegant solution is to use the incoming IAsyncResult parameter.
The incoming IAsyncResult parameter passed into the target of the AsyncCallback delegate is actually an instance of the AsyncResult class (note the lack of an I prefix) defined in the System. Runtime.Remoting.Messaging namespace. The static AsyncDelegate property returns a reference to


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 |
459 |
Cool! Now that you understand how a .NET delegate can be used to automatically spin off
a secondary thread of execution to handle an asynchronous method invocation, let’s turn our attention to directly interacting with threads using the System.Threading namespace.
■Source Code The AsyncCallbackDelegate project is located under the Chapter 14 subdirectory.
The System.Threading Namespace
Under the .NET platform, the System.Threading namespace provides a number of types that enable the construction of multithreaded applications. In addition to providing types that allow you to interact with a particular CLR thread, this namespace defines types that allow access to the CLR maintained thread pool, a simple (non–GUI-based) Timer class, and numerous types used to provide synchronized access to shared resources. Table 14-1 lists some of the core members of this namespace. (Be sure to consult the .NET Framework 2.0 SDK documentation for full details.)
Table 14-1. Select Types of the System.Threading Namespace
Type |
Meaning in Life |
Interlocked |
This type provides atomic operations for types that are shared by |
|
multiple threads. |
Monitor |
This type provides the synchronization of threading objects using |
|
locks and wait/signals. The C# lock keyword makes use of |
|
a Monitor type under the hood. |
Mutex |
This synchronization primitive can be used for synchronization |
|
between application domain boundaries. |
ParameterizedThreadStart |
This delegate (which is new to .NET 2.0) allows a thread to call |
|
methods that take any number of arguments. |
Semaphore |
This type allows you to limit the number of threads that can |
|
access a resource, or a particular type of resource, concurrently. |
Thread |
This type represents a thread that executes within the CLR. Using |
|
this type, you are able to spawn additional threads in the |
|
originating AppDomain. |
ThreadPool |
This type allows you to interact with the CLR-maintained thread |
|
pool within a given process. |
ThreadPriority |
This enum represents a thread’s priority level (Highest, Normal, etc.). |
ThreadStart |
This delegate is used to specify the method to call for a given |
|
thread. Unlike the ParameterizedThreadStart delegate, targets of |
|
ThreadStart must match a fixed prototype. |
ThreadState |
This enum specifies the valid states a thread may take (Running, |
|
Aborted, etc.). |
Timer |
This type provides a mechanism for executing a method at |
|
specified intervals. |
TimerCallback |
This delegate type is used in conjunction with Timer types. |
|
|


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 |
461 |
application named ThreadStats. As you know, the static Thread.CurrentThread property retrieves a Thread type that represents the currently executing thread. Once you have obtained the current thread, you are able to print out various statistics:
// Be sure to 'use' the System.Threading namespace. static void Main(string[] args)
{
Console.WriteLine("***** Primary Thread stats *****\n");
//Obtain and name the current thread.
Thread primaryThread = Thread.CurrentThread; primaryThread.Name = "ThePrimaryThread";
//Show details of hosting AppDomain/Context.
Console.WriteLine("Name of current AppDomain: {0}", Thread.GetDomain().FriendlyName);
Console.WriteLine("ID of current Context: {0}", Thread.CurrentContext.ContextID);
//Print out some stats about this thread.
Console.WriteLine("Thread Name: {0}", primaryThread.Name);
Console.WriteLine("Has thread started?: {0}", primaryThread.IsAlive);
Console.WriteLine("Priority Level: {0}", primaryThread.Priority);
Console.WriteLine("Thread State: {0}", primaryThread.ThreadState);
Console.ReadLine();
}
Figure 14-5 shows the output for the current application.
Figure 14-5. Gathering thread statistics
The Name Property
While this code is more or less self-explanatory, do notice that the Thread class supports a property called Name. If you do not set this value, Name will return an empty string. However, once you assign a friendly string moniker to a given Thread object, you can greatly simplify your debugging endeavors. If you are making use of Visual Studio 2005, you may access the Threads window during a debugging session (select Debug Windows Threads). As you can see from Figure 14-6, you can quickly identify the thread you wish to diagnose.

462 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
Figure 14-6. Debugging a thread with Visual Studio 2005
The Priority Property
Next, notice that the Thread type defines a property named Priority. By default, all threads have a priority level of Normal. However, you can change this at any point in the thread’s lifetime using the ThreadPriority property and the related System.Threading.ThreadPriority enumeration:
public enum ThreadPriority
{
AboveNormal,
BelowNormal,
Highest,
Idle,
Lowest,
Normal, |
// Default value. |
TimeCritical
}
If you were to assign a thread’s priority level to a value other than the default (ThreadPriority. Normal), understand that you would have little control over when the thread scheduler switches between threads. In reality, a thread’s priority level offers a hint to the CLR regarding the importance of the thread’s activity. Thus, a thread with the value ThreadPriority.Highest is not necessarily guaranteed to given the highest precedence.
Again, if the thread scheduler is preoccupied with a given task (e.g., synchronizing an object, switching threads, or moving threads), the priority level will most likely be altered accordingly. However, all things being equal, the CLR will read these values and instruct the thread scheduler how to best allocate time slices. All things still being equal, threads with an identical thread priority should each receive the same amount of time to perform their work.
In most cases, you will seldom (if ever) need to directly alter a thread’s priority level. In theory, it is possible to jack up the priority level on a set of threads, thereby preventing lower-priority threads from executing at their required levels (so use caution).
■Source Code The ThreadStats project is included under the Chapter 14 subdirectory.
Programmatically Creating Secondary Threads
When you wish to programmatically create additional threads to carry on some unit of work, you will follow a very predictable process:
1.Create a type method to be the entry point for the new thread.
2.Create a new ParameterizedThreadStart (or legacy ThreadStart) delegate, passing the address of the method defined in step 1 to the constructor.

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 |
463 |
3.Create a Thread object, passing the ParameterizedThreadStart/ThreadStart delegate as a constructor argument.
4.Establish any initial thread characteristics (name, priority, etc.).
5.Call the Thread.Start() method. This starts the thread at the method referenced by the delegate created in step 2 as soon as possible.
As stated in step 2, you may make use of two distinct delegate types to “point to” the method that the secondary thread will execute. The ThreadStart delegate has been part of the System.Threading namespace since .NET 1.0, and it can point to any method that takes no arguments and returns nothing. This delegate can be helpful when the method is designed to simply run in the background without further interaction.
The obvious limitation of ThreadStart is that you are unable to pass in parameters for processing. As of .NET 2.0, you are provided with the ParameterizedThreadStart delegate type, which allows a single parameter of type System.Object. Given that anything can be represented as a System.Object, you can pass in any number of parameters via a custom class or structure. Do note, however, that the ParameterizedThreadStart delegate can only point to methods that return void.
Working with the ThreadStart Delegate
To illustrate the process of building a multithreaded application (as well as to demonstrate the usefulness of doing so), assume you have a console application (SimpleMultiThreadApp) that allows the end user to choose whether the application will perform its duties using the single primary thread or split its workload using two separate threads of execution.
Assuming you have “used” the System.Threading namespace via the C# using keyword, your first step is to define a type method to perform the work of the (possible) secondary thread. To keep focused on the mechanics of building multithreaded programs, this method will simply print out a sequence of numbers to the console window, pausing for approximately two seconds with each pass. Here is the full definition of the Printer class:
public class Printer
{
public void PrintNumbers()
{
// 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++)
{
Console.Write(i + ", "); Thread.Sleep(2000);
}
Console.WriteLine();
}
}
Now, within Main(), you will first prompt the user to determine if one or two threads will be used to perform the application’s work. If the user requests a single thread, you will simply invoke the PrintNumbers() method within the primary thread. However, if the user specifies two threads, you will create a ThreadStart delegate that points to PrintNumbers(), pass this delegate object into the constructor of a new Thread object, and call Start() to inform the CLR this thread is ready for processing.