Добавил:
Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:

Pro CSharp 2008 And The .NET 3.5 Platform [eng]

.pdf
Скачиваний:
78
Добавлен:
16.08.2013
Размер:
22.5 Mб
Скачать

592 CHAPTER 18 BUILDING MULTITHREADED APPLICATIONS

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 18-3).

Figure 18-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 member variable in the class 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 the original asynchronous delegate that was created elsewhere. Therefore, if you wish to obtain a reference to the BinaryOp delegate object allocated within Main(), simply cast the System.Object returned by the AsyncDelegate property into type BinaryOp. At this point, you can trigger

EndInvoke() as expected:

//Don't forget to import

//System.Runtime.Remoting.Messaging! static void AddComplete(IAsyncResult itfAR)

{

Console.WriteLine("AddComplete() invoked on thread {0}.", Thread.CurrentThread.ManagedThreadId);

Console.WriteLine("Your addition is complete");

// Now get the result.

AsyncResult ar = (AsyncResult)itfAR; BinaryOp b = (BinaryOp)ar.AsyncDelegate;

Console.WriteLine("10 + 10 is {0}.", b.EndInvoke(itfAR));

}

Passing and Receiving Custom State Data

The final aspect of asynchronous delegates we need to address is the final argument to the BeginInvoke() method (which has been null up to this point). This parameter allows you to pass additional state information to the callback method from the primary thread. Because this argument is prototyped as a System.Object, you can pass in any type of data whatsoever, as long as the callback method knows what to expect. Assume for the sake of demonstration that the primary thread wishes to pass in a custom text message to the AddComplete() method:

CHAPTER 18 BUILDING MULTITHREADED APPLICATIONS

593

static void Main(string[] args)

{

...

IAsyncResult iftAR = b.BeginInvoke(10, 10, new AsyncCallback(AddComplete),

"Main() thanks you for adding these numbers.");

...

}

To obtain this data within the scope of AddComplete(), make use of the AsyncState property of the incoming IAsyncResult parameter. Notice that an explicit cast will be required; therefore the primary and secondary threads must agree on the underlying type returned from AsyncState.

static void AddComplete(IAsyncResult itfAR)

{

...

// Retrieve the informational object and cast it to string. string msg = (string)itfAR.AsyncState; Console.WriteLine(msg);

}

Figure 18-4 shows the output of the current application.

Figure 18-4. Passing and receiving custom state data

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 18 subdirectory.

The System.Threading Namespace

Under the .NET platform, the System.Threading namespace provides a number of types that enable the direct 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 18-1 lists some of the core members of this namespace. (Be sure to consult the .NET Framework 3.5 SDK documentation for full details.)

594 CHAPTER 18 BUILDING MULTITHREADED APPLICATIONS

Table 18-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 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.

 

 

The System.Threading.Thread Class

The most primitive of all types in the System.Threading namespace is Thread. This class represents an object-oriented wrapper around a given path of execution within a particular AppDomain. This type also defines a number of methods (both static and shared) that allow you to create new threads within the current AppDomain, as well as to suspend, stop, and destroy a particular thread. Consider the list of core static members in Table 18-2.

Table 18-2. Key Static Members of the Thread Type

Static Member

Meaning in Life

CurrentContext

This read-only property returns the context in which the thread is currently

 

running.

CurrentThread

This read-only property returns a reference to the currently running thread.

GetDomain()

These methods return a reference to the current AppDomain or the ID of this

GetDomainID()

domain in which the current thread is running.

Sleep()

This method suspends the current thread for a specified time.

 

 

CHAPTER 18 BUILDING MULTITHREADED APPLICATIONS

595

The Thread class also supports several instance-level members, some of which are shown in Table 18-3.

Table 18-3. Select Instance-Level Members of the Thread Type

Instance-Level Member

Meaning in Life

IsAlive

Returns a Boolean that indicates whether this thread has been started.

IsBackground

Gets or sets a value indicating whether or not this thread is a

 

“background thread” (more details in just a moment).

Name

Allows you to establish a friendly text name of the thread.

Priority

Gets or sets the priority of a thread, which may be assigned a value from

 

the ThreadPriority enumeration.

ThreadState

Gets the state of this thread, which may be assigned a value from the

 

ThreadState enumeration.

Abort()

Instructs the CLR to terminate the thread as soon as possible.

Interrupt()

Interrupts (e.g., wakes ) the current thread from a suitable wait period.

Join()

Blocks the calling thread until the specified thread (the one on which

 

Join() is called) exits.

Resume()

Resumes a thread that has been previously suspended.

Start()

Instructs the CLR to execute the thread ASAP.

Suspend()

Suspends the thread. If the thread is already suspended, a call to

 

Suspend() has no effect.

 

 

Note Aborting or suspending an active thread is generally considered a bad idea. When you do so, there is a chance (however small) that a thread could “leak” its workload when disturbed or terminated.

Obtaining Statistics About the Current Thread

Recall that the entry point of an executable assembly (i.e., the Main() method) runs on the primary thread of execution. To illustrate the basic use of the Thread type, assume you have a new Console 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 import 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);

596 CHAPTER 18 BUILDING MULTITHREADED APPLICATIONS

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 18-5 shows the output for the current application.

Figure 18-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 2008, you may access the Threads window during a debugging session (select Debug Windows Threads). As you can see from Figure 18-6, you can quickly identify the thread you wish to diagnose.

Figure 18-6. Debugging a thread with Visual Studio 2008

CHAPTER 18 BUILDING MULTITHREADED APPLICATIONS

597

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 no direct 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 be 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. 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 18 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.

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.

598CHAPTER 18 BUILDING MULTITHREADED APPLICATIONS

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, we were 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 imported the System.Threading namespace, 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("{0}, ", i); Thread.Sleep(2000);

}

Console.WriteLine();

}

}

Now, within Main(), you will first prompt the user to determine whether 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.

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();

CHAPTER 18 BUILDING MULTITHREADED APPLICATIONS

599

// 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 18-7).

Figure 18-7. Multithreaded applications provide results in more responsive applications.

600 CHAPTER 18 BUILDING MULTITHREADED APPLICATIONS

Before we move on, it is important to note that when you build multithreaded applications (which includes the use of asynchronous delegates to do so) 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 this particular 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 18 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 import 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, 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:

static void Add(object data)

{

if (data is AddParams)

{

Console.WriteLine("ID of thread in Main(): {0}", Thread.CurrentThread.ManagedThreadId);

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:

CHAPTER 18 BUILDING MULTITHREADED APPLICATIONS

601

static void Main(string[] args)

{

Console.WriteLine("***** Adding with Thread objects *****");

Console.WriteLine("ID of thread in Main(): {0}", Thread.CurrentThread.ManagedThreadId);

// Make an AddParams object to pass to the secondary thread.

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 18 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:

static void Main(string[] args)

{

Console.WriteLine("***** Background Threads *****\n");

Printer p = new Printer(); Thread bgroundThread =

new Thread(new ThreadStart(p.PrintNumbers));

// This is now a background thread. bgroundThread.IsBackground = true; bgroundThread.Start();

}