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

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

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

582 CHAPTER 17 PROCESSES, APPDOMAINS, AND OBJECT CONTEXTS

Summary

The point of this chapter was to examine exactly how a .NET-executable image is hosted by the

.NET platform. As you have seen, the long-standing notion of a Win32 process has been altered under the hood to accommodate the needs of the CLR. A single process (which can be programmatically manipulated via the System.Diagnostics.Process type) is now composed of multiple application domains, which represent isolated and independent boundaries within a process.

As you have seen, a single process can host multiple application domains, each of which is capable of hosting and executing any number of related assemblies. Furthermore, a single application domain can contain any number of contextual boundaries. Using this additional level of type isolation, the CLR can ensure that special-need objects are handled correctly.

C H A P T E R 1 8

Building Multithreaded Applications

In the previous chapter, you examined the relationship between processes, application domains, and contexts. This chapter builds on your newfound knowledge by examining how the .NET platform allows you to build multithreaded applications and examines various ways to keep shared resources thread-safe.

You’ll begin by revisiting the .NET delegate type and come to understand its intrinsic support for asynchronous method invocations. As you’ll see, this technique allows you to invoke a method on a secondary thread of execution automatically. Next, you’ll investigate the types within the System.Threading namespace. Here you’ll examine numerous types (Thread, ThreadStart, etc.) that allow you to easily create additional threads of execution. As well, you will come to understand the use of the BackgroundWorker type, which simplifies the task of performing background operations within the context of a GUI-based application.

Of course, the complexity of multithreaded development isn’t in the creation of threads, but in ensuring that your code base is well equipped to handle concurrent access to shared resources. Given this, the chapter also examines various synchronization primitives that the .NET Framework provides.

The Process/AppDomain/Context/Thread

Relationship

In the previous chapter, a thread was defined as a path of execution within an executable application. While many .NET applications can live happy and productive single-threaded lives, an assembly’s primary thread (spawned by the CLR when Main() executes) may create secondary threads of execution to perform additional units of work. By implementing additional threads, you can build more responsive (but not necessarily faster executing on single-core machines) applications.

Note These days it is quite common for new computers to make use of multicore processors (or at very least a hyperthreaded single-core processor). Without making use of multiple threads, developers are unable to exploit the full power of multicore machines.

The System.Threading namespace contains various types that allow you to create multithreaded applications. The Thread class is perhaps the core type, as it represents a given thread. If you wish to programmatically obtain a reference to the thread currently executing a given member, simply call the static Thread.CurrentThread property:

583

584CHAPTER 18 BUILDING MULTITHREADED APPLICATIONS

static void ExtractExecutingThread()

{

//Get the thread currently

//executing this method.

Thread currThread = Thread.CurrentThread;

}

Under the .NET platform, there is not a direct one-to-one correspondence between application domains and threads. In fact, a given AppDomain can have numerous threads executing within it at any given time. Furthermore, a particular thread is not confined to a single application domain during its lifetime. Threads are free to cross application domain boundaries as the Win32 thread scheduler and CLR see fit.

Although active threads can be moved between AppDomain boundaries, a given thread can execute within only a single application domain at any point in time (in other words, it is impossible for a single thread to be doing work in more than one AppDomain at once). When you wish to programmatically gain access to the AppDomain that is hosting the current thread, call the static

Thread.GetDomain() method:

static void ExtractAppDomainHostingThread()

{

// Obtain the AppDomain hosting the current thread.

AppDomain ad = Thread.GetDomain();

}

A single thread may also be moved into a particular context at any given time, and it may be relocated within a new context at the whim of the CLR. When you wish to obtain the current context a thread happens to be executing in, make use of the static Thread.CurrentContext property:

static void ExtractCurrentThreadContext()

{

//Obtain the context under which the

//current thread is operating.

Context ctx = Thread.CurrentContext;

}

Again, the CLR is the entity that is in charge of moving threads into (and out of) application domains and contexts. As a .NET developer, you can usually remain blissfully unaware where a given thread ends up (or exactly when it is placed into its new boundary). Nevertheless, you should be aware of the various ways of obtaining the underlying primitives.

The Problem of Concurrency

One of the many “joys” (read: painful aspects) of multithreaded programming is that you have little control over how the underlying operating system or the CLR makes use of its threads. For example, if you craft a block of code that creates a new thread of execution, you cannot guarantee that the thread executes immediately. Rather, such code only instructs the OS to execute the thread as soon as possible (which is typically when the thread scheduler gets around to it).

Furthermore, given that threads can be moved between application and contextual boundaries as required by the CLR, you must be mindful of which aspects of your application are threadvolatile (e.g., subject to multithreaded access) and which operations are atomic (thread-volatile operations are the dangerous ones!). To illustrate the problem, assume a thread is invoking a method of a specific object. Now assume that this thread is instructed by the thread scheduler to suspend its activity, in order to allow another thread to access the same method of the same object.

If the original thread was not completely finished with its operation, the second incoming thread may be viewing an object in a partially modified state. At this point, the second thread is

CHAPTER 18 BUILDING MULTITHREADED APPLICATIONS

585

basically reading bogus data, which is sure to give way to extremely odd (and very hard to find) bugs, which are even harder to replicate and debug.

Atomic operations, on the other hand, are always safe in a multithreaded environment. Sadly, there are very few operations in the .NET base class libraries that are guaranteed to be atomic. Even the act of assigning a value to a member variable is not atomic! Unless the .NET Framework 3.5 SDK documentation specifically says an operation is atomic, you must assume it is thread-volatile and take precautions.

The Role of Thread Synchronization

At this point, it should be clear that multithreaded application domains are in themselves quite volatile, as numerous threads can operate on the shared functionality at (more or less) the same time. To protect an application’s resources from possible corruption, .NET developers must make use of any number of threading primitives (such as locks, monitors, and the [Synchronization] attribute) to control access among the executing threads.

Although the .NET platform cannot make the difficulties of building robust multithreaded applications completely disappear, the process has been simplified considerably. Using types defined within the System.Threading namespace, you are able to spawn additional threads with minimal fuss and bother. Likewise, when it is time to lock down shared points of data, you will find additional types that provide the same functionality as the Win32 API threading primitives (using a much cleaner object model).

However, the System.Threading namespace is not the only way to build multithreaded .NET programs. During our examination of the .NET delegate (see Chapter 11), it was mentioned that all delegates have the ability to invoke members asynchronously. This is a major benefit of the .NET platform, given that one of the most common reasons a developer creates threads is for the purpose of invoking methods in a nonblocking (a.k.a. asynchronous) manner. Although you could make use of the System.Threading namespace to achieve a similar result, delegates make the whole process much easier.

A Brief Review of the .NET Delegate

Recall that the .NET delegate type is essentially a type-safe object-oriented function pointer. When you declare a .NET delegate, the C# compiler responds by building a sealed class that derives from System.MulticastDelegate (which in turn derives from System.Delegate). These base classes provide every delegate with the ability to maintain a list of method addresses, all of which may be invoked at a later time. Consider the BinaryOp delegate first defined in Chapter 11:

// A C# delegate type.

public delegate int BinaryOp(int x, int y);

Based on its definition, BinaryOp can point to any method taking two integers (by value) as arguments and returning an integer. Once compiled, the defining assembly now contains a fullblown class definition that is dynamically generated based on the delegate declaration. In the case of BinaryOp, this class looks more or less like the following (shown in pseudo-code):

public sealed class BinaryOp : System.MulticastDelegate

{

public BinaryOp(object target, uint functionAddress); public void Invoke(int x, int y);

public IAsyncResult BeginInvoke(int x, int y, AsyncCallback cb, object state);

public int EndInvoke(IAsyncResult result);

}

586 CHAPTER 18 BUILDING MULTITHREADED APPLICATIONS

Recall that the generated Invoke() method is used to invoke the methods maintained by a delegate object in a synchronous manner. Therefore, the calling thread (such as the primary thread of the application) is forced to wait until the delegate invocation completes. Also recall that in C#, the Invoke() method does not need to be directly called in code, but can be triggered indirectly under the hood when applying “normal” method invocation syntax. Consider the following console program (SyncDelegateReview), which invokes the static Add() method in a synchronous (a.k.a. blocking) manner:

// Need this for the Thread.Sleep() call. using System.Threading;

using System;

namespace SyncDelegate

{

public delegate int BinaryOp(int x, int y);

class Program

{

static void Main(string[] args)

{

Console.WriteLine("***** Synch Delegate Review *****");

//Print out the ID of the executing thread.

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

//Invoke Add() in a synchronous manner.

BinaryOp b = new BinaryOp(Add);

//Could also write b.Invoke(10, 10); int answer = b(10, 10);

//These lines will not execute until

//the Add() method has completed.

Console.WriteLine("Doing more work in Main()!"); Console.WriteLine("10 + 10 is {0}.", answer); Console.ReadLine();

}

static int Add(int x, int y)

{

//Print out the ID of the executing thread.

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

//Pause to simulate a lengthy operation.

Thread.Sleep(5000); return x + y;

}

}

}

Notice first of all that this program is making use of the System.Threading namespace to gain access to the Thread type. Within the Add() method, you are invoking the static Thread.Sleep() method to suspend the calling thread for approximately five seconds to simulate a lengthy task. Given that you are invoking the Add() method in a synchronous manner, the Main() method will not print out the result of the operation until the Add() method has completed.

CHAPTER 18 BUILDING MULTITHREADED APPLICATIONS

587

Next, note that the Main() method is obtaining access to the current thread (via Thread. CurrentThread) and printing out the ID of the thread via the ManagedThreadId property. This same logic is repeated in the static Add() method. As you might suspect, given that all the work in this application is performed exclusively by the primary thread, you find the same ID value displayed to the console (see Figure 18-1).

Figure 18-1. Synchronous method invocations are “blocking” calls.

When you run this program, you should notice that a five-second delay takes place before you see the final Console.WriteLine() logic in Main() execute. Although many (if not most) methods may be called synchronously without ill effect, .NET delegates can be instructed to call their methods asynchronously if necessary.

Source Code The SyncDelegateReview project is located under the Chapter 18 subdirectory.

The Asynchronous Nature of Delegates

If you are new to the topic of multithreading, you may wonder what exactly an asynchronous method invocation is all about. As you are no doubt fully aware, some programming operations take time. Although the previous Add() was purely illustrative in nature, imagine that you built a single-threaded application that is invoking a method on a remote object, performing a longrunning database query, downloading a large document, or writing 500 lines of text to an external file. While performing these operations, the application will appear to hang for some amount of time. Until the task at hand has been processed, all other aspects of this program (such as menu activation, toolbar clicking, or console output) are unresponsive.

The question therefore is, how can you tell a delegate to invoke a method on a separate thread of execution to simulate numerous tasks performing “at the same time”? The good news is that every .NET delegate type is automatically equipped with this capability. The even better news is that you are not required to directly dive into the details of the System.Threading namespace to do so (although these entities can quite naturally work hand in hand).

The BeginInvoke() and EndInvoke() Methods

When the C# compiler processes the delegate keyword, the dynamically generated class defines two methods named BeginInvoke() and EndInvoke(). Given our definition of the BinaryOp delegate, these methods are prototyped as follows:

public sealed class BinaryOp : System.MulticastDelegate

{

...

// Used to invoke a method asynchronously.

588 CHAPTER 18 BUILDING MULTITHREADED APPLICATIONS

public IAsyncResult BeginInvoke(int x, int y, AsyncCallback cb, object state);

//Used to fetch the return value

//of the invoked method.

public int EndInvoke(IAsyncResult result);

}

The first stack of parameters passed into BeginInvoke() will be based on the format of the C# delegate (two integers in the case of BinaryOp). The final two arguments will always be System.AsyncCallback and System.Object. We’ll examine the role of these parameters shortly; for

the time being, though, we’ll supply null for each. Also note that the return value of EndInvoke() is an integer, based on the definition of BinaryOp, while the parameter of this method is of type

IAsyncResult.

The System.IAsyncResult Interface

The BeginInvoke() method always returns an object implementing the IAsyncResult interface, while EndInvoke() requires an IAsyncResult-compatible type as its sole parameter. The IAsyncResult-compatible object returned from BeginInvoke() is basically a coupling mechanism that allows the calling thread to obtain the result of the asynchronous method invocation at a later time via EndInvoke(). The IAsyncResult interface (defined in the System namespace) is defined as follows:

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 provides a void 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}.",

CHAPTER 18 BUILDING MULTITHREADED APPLICATIONS

589

Thread.CurrentThread.ManagedThreadId);

//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 thread IDs are displayed, given that there are in fact multiple threads working within the current AppDomain (see Figure 18-2).

Figure 18-2. Methods invoked asynchronously are done so on a unique thread.

In addition to the unique ID 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.

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 five seconds!

Console.WriteLine("Doing more work in Main()!");

//The calling thread is now blocked until

//BeginInvoke() completes.

int answer = b.EndInvoke(iftAR);

...

}

590 CHAPTER 18 BUILDING MULTITHREADED APPLICATIONS

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 whether 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()!"); Thread.Sleep(1000);

}

//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. The call to Thread.Sleep(1000) is not necessary for this particular application to function correctly; however, by forcing the primary thread to wait for approximately one second during each iteration, it prevents the same message from printing hundreds of times.

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, which no longer makes use of a call to

Thread.Sleep():

while (!iftAR.AsyncWaitHandle.WaitOne(1000, 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 elegant) techniques to obtain the result of a method that has been called asynchronously.

Source Code The AsyncDelegate project is located under the Chapter 18 subdirectory.

CHAPTER 18 BUILDING MULTITHREADED APPLICATIONS

591

The Role of the AsyncCallback Delegate

Rather than polling a delegate to determine whether an asynchronously invoked method has completed, it would be more efficient to have the secondary thread 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:

// Targets of AsyncCallback must match the following pattern. 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 whether 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 *****");

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

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.ManagedThreadId);

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

}

static int Add(int x, int y)

{

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

return x + y;

}

}

}