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

Pro CSharp And The .NET 2.0 Platform (2005) [eng]

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

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

458C 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

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 add a 'using' directive for

//System.Runtime.Remoting.Messaging!

static void AddComplete(IAsyncResult itfAR)

{

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

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:

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:

static void AddComplete(IAsyncResult itfAR)

{

...

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

}

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

Figure 14-4. Passing and receiving custom state data

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.

 

 

460 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

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 14-2.

Table 14-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

GetDomainID()

of this domain in which the current thread is running.

Sleep()

This method suspends the current thread for a specified time.

 

 

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

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

 

 

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

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.