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

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

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

444 C H A P T E R 1 3 P R O C E S S E S, A P P D O M A I N S, C O N T E X T S, A N D C L R H O S T S

Given that the SportsCar class has not been qualified with a context attribute, the CLR has allocated sport and sport2 into context 0 (i.e., the default context). However, the SportsCarTS object is loaded into a unique contextual boundary (which has been assigned a context ID of 1), given the fact that this context-bound type was adorned with the [Synchronization] attribute.

Source Code The ContextManipulator project is included under the Chapter 13 subdirectory

Summarizing Processes, AppDomains, and Context

At this point, you hopefully have a much better idea about how a .NET assembly is hosted by the CLR. To summarize the key points,

A .NET process hosts one to many application domains. Each AppDomain is able to host any number of related .NET assemblies and may be independently loaded and unloaded by the CLR (or programmatically via the System.AppDomain type).

A given AppDomain consists of one to many contexts. Using a context, the CLR is able to place a “special needs” object into a logical container, to ensure that its runtime requirements are honored.

If the previous pages have seemed to be a bit too low level for your liking, fear not. For the most part, the .NET runtime automatically deals with the details of processes, application domains, and contexts on your behalf. The good news, however, is that this information provides a solid foundation for understanding multithreaded programming under the .NET platform. Before we turn our attention to the System.Threading namespace, though, we’ll examine how the CLR itself is hosted by the Win32 OS.

Hosting the Common Language Runtime

To the end user, running a .NET executable is achieved simply by double-clicking the *.exe in Windows Explorer (or activating an associated shortcut). As you recall from Chapter 1, however, the .NET Framework is not (currently) incorporated directly into the Windows OS, but sits on top of the OS itself. When you install Visual Studio 2005 (or the .NET Framework 2.0 SDK) on your development machine, the .NET runtime environment (including the necessary base class libraries) is installed as well. Also recall that Microsoft provides a freely distributable .NET runtime setup program (dotnetfx.exe) to configure end user machines to host .NET assemblies.

Given that the Windows OS does not natively understand the format of a .NET assembly, it should be clear that various steps occur in the background when an executable assembly is activated. Under the Windows XP OS, the basic steps are as follows (do recall from Chapter 11 that all .NET assemblies contain Win32 header information):

1.The Windows OS loads the executable binary file into memory.

2.The Windows OS reads the embedded WinNT header to determine if the binary file is a .NET assembly (via the IMAGE_DIRECTORY_ENTRY_COM_DESCRIPTOR flag).

3.If the image is a .NET assembly, mscoree.dll is loaded.

4.mscoree.dll then loads one of two implementations of the CLR (mscorwks.dll or mscorsvr.dll).

5.At this point, the CLR takes over the show, performing all .NET-centric details (finding external assemblies, performing security checks, processing CIL code, performing garbage collections, etc.).

C H A P T E R 1 3 P R O C E S S E S, A P P D O M A I N S, C O N T E X T S, A N D C L R H O S T S

445

As suggested by the previous list, mscoree.dll is not the CLR itself (as I have suggested in previous chapters). Although it is safe to regard mscoree.dll as the actual CLR, in reality this binary file is a shim to one of two possible CLR implementations. If the host machine makes use of a single CPU, mscorwks.dll is loaded. If the machine supports multiple CPUs, mscorsvr.dll is loaded into memory (which is a version of the CLR optimized to execute on multiple-processor machines).

Side-by-Side Execution of the CLR

To dig just a bit deeper, realize that the .NET platform supports side-by-side execution, meaning that multiple versions of the .NET platform can be installed on a single machine (1.0, 1.1, and 2.0 at the time of this writing). mscoree.dll itself resides in the machine’s System32 subdirectory of the registered Windows installation directory. On my machine, mscoree.dll lives under C:\WINDOWS\ system32 (see Figure 13-11).

Figure 13-11. mscoree.dll lives under the System32 directory

Once mscoree.dll has been loaded, the Win32 system registry (yes, that system registry) is consulted to determine the latest installed version and installation path of the .NET Framework via HKEY_LOCAL_MACHINE\Software\Microsoft\.NETFramework (see Figure 13-12).

Figure 13-12. Resolving the version and installation path of the .NET platform

446 C H A P T E R 1 3 P R O C E S S E S, A P P D O M A I N S, C O N T E X T S, A N D C L R H O S T S

Once the version and installation path of the .NET platform have been determined, the correct version of mscorwks.dll/mscorsvr.dll is loaded into memory. Again, on my machine, the root installation path of the .NET platform is C:\WINDOWS\Microsoft.NET\Framework. Under this directory are specific subdirectories for .NET version 1.0, 1.1, and (at the time of this writing) the current build of 2.0 (see Figure 13-13; your version numbers may differ).

Figure 13-13. mscorwks.dll version 2.0

Loading a Specific Version of the CLR

When mscoree.dll determines which version of mscorwks.dll/mscorsrv.dll to load (by consulting the system registry), it will also read a subfolder under HKEY_LOCAL_MACHINE\Software\Microsoft\

.NET\Framework named “policy.” This subfolder records the CLR upgrades that may be safely performed. For example, if you execute an assembly that was built using .NET version 1.0.3705, mscoree.dll learns from the policy file that it can safely load version 1.1.4322.

This promotion occurs silently in the background and only when the upgrade is known to produce compatible execution. In the rare case that you wish to instruct mscoree.dll to load a specific version of the CLR, you may do so using a client-side *.config file:

<?xml version="1.0" encoding="utf-8" ?> <configuration>

<startup>

<requiredRuntime version ="1.0.3705"/>

</startup>

</configuration>

Here, the <requiredRuntime> element expresses that only version 1.0.3705 should be used to host the assembly in question. Therefore, if the target machine does not have a complete installation of .NET version 1.0.3705, the end user is presented with the runtime error shown in Figure 13-14.

Figure 13-14. <requiredRuntime> results in a runtime error if the specified version of the CLR is not

C H A P T E R 1 3 P R O C E S S E S, A P P D O M A I N S, C O N T E X T S, A N D C L R H O S T S

447

Additional CLR Hosts

The process just defined qualifies the basic steps taken by the Windows OS to host the CLR when an executable assembly is activated. However, Microsoft provides many applications that bypass this out-of-the-box behavior in favor of loading the CLR programmatically. For example, Microsoft Internet Explorer can natively host custom Windows Forms controls (the managed equivalent of the now legacy ActiveX controls). The latest version of Microsoft SQL Server (code-named Yukon and officially called SQL Server 2005) also has the ability to directly host the CLR internally.

As a final note, Microsoft has defined a set of interfaces that allow developers to build their own custom CLR host. This may be done using straight C/C++ code or via a COM type library (mscoree.tlb). While the process of building a custom CLR host is surprisingly simple (especially using the COM type library), this topic is outside the scope of this text. If you require further information, you can find numerous articles online (just do a search for “CLR hosts”).

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. The chapter concluded by examining the details regarding how the CLR is hosted by the Win32 OS.

C H A P T E R 1 4

■ ■ ■

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 how 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. 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 closes by examining 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) applications.

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:

private 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.

449

450 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

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). When you wish to programmatically gain access to the AppDomain that is hosting the current thread, call the static

Thread.GetDomain() method:

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

private 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 and the Role of Thread

Synchronization

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 thread-volatile (e.g., subject to multithreaded access) and which operations are atomic (thread-volatile operations are the dangerous ones!). To illustrate, 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 the current operation, the second incoming thread may be viewing an object in a partially modified state. At this point, the second thread is 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 2.0 SDK documentation specifically says an operation is atomic, you must assume it is thread-volatile and take precautions.

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

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

451

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 multithread .NET programs. During our examination of the .NET delegate (see Chapter 8), 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 (aka 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 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 8:

// 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 as arguments and returning an integer. Once compiled, the defining assembly now contains a full-blown 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):

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

}

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 is not directly called in code, but is triggered under the hood when applying “normal” method invocation syntax. Consider the following program, which invokes the static Add() method in a synchronous (aka blocking) manner:

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

using System;

namespace SyncDelegate

{

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

452 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

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

//Invoke Add() in a synchronous manner.

BinaryOp b = new BinaryOp(Add); 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.GetHashCode());

//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. Within the Add() method, you are invoking the static Thread.Sleep() method to suspend the calling thread for (more or less) 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.

Next, note that the Main() method is obtaining access to the current thread (via Thread. CurrentThread) and printing out its hash code. Given that a hash code represents an object in a specific state, this value can be used to as a quick-and-dirty thread ID. 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 hash code value displayed to the console (see Figure 14-1).

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

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

453

When you run this program, you should notice that a five-second delay takes place before you see the Console.WriteLine() logic 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 SyncDelegate project is located under the Chapter 14 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 long-running database query, or writing 500 lines of text to an external file. While performing these operations, the application will appear to hang for quite some 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 so:

sealed class BinaryOp : System.MulticastDelegate

{

...

//Used to invoke a method asynchronously. 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.

The System.IAsyncResult Interface

Also note that 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: