|
|||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Announcements
Want a new Job?
Chapters
Services
Feature Zones
|
Assemblies, Threads, and AppDomainsEach of the applications developed during the first five chapters are along the lines of traditional 'stand alone' applications, given that all programming logic was contained within a single (EXE) binary. One aspect of the .NET lifestyle is the notion of binary reuse. Like COM, .NET provides the ability to access types between binaries in a language-independent manner. However, the .NET platform provides far greater language integration than classic COM. For example, the .NET platform supports cross-language inheritance (imagine a Visual Basic.NET class deriving from a C# class). To understand how this is achieved requires a deeper understanding of assemblies. Once you understand the logical and physical layout of an assembly (and the related manifest), you then learn to distinguish between 'private' and 'shared' assemblies. You also examine exactly how the .NET runtime resolves the location of an assembly and come to understand the role of the Global Assembly Cache (GAC). Closely related to location resolution is the notion of application configuration files. As you will see, the .NET runtime can read the XML-based data contained within this file to bind to a specific version of a shared assembly (among other things). This chapter wraps up with an examination of building multithreaded assemblies, using the types defined within the System.Threading namespace. If you are coming from a Win32 background, you will be pleased to see how nicely thread manipulation has cleaned up under the .NET framework. Problems with Classic COM BinariesBinary reuse (i.e., portable code libraries) is not a new idea. To date, the most popular way in which a programmer can share types between binaries (and in some respects, across languages) is to build what can now be regarded as 'classic COM servers.' Although the construction and use of COM binaries is a well-established industry standard, these little blobs have caused each of us a fair share of headaches. Beyond the fact that COM demands a good deal of complex infrastructure (IDL, class factories, scripting support, and so forth), I am sure you have also pondered the following related questions:
The .NET framework greatly improves on the current state of affairs and addresses the versioning and deployment problems head-on using a new binary format termed an assembly. However, before you come to understand how the assembly offers a clean solution to these issues, let's spend some time recapping the problems in a bit more detail. Problem: COM VersioningIn COM, you build entities named coclasses that are little more than a custom UDT (user defined type) implementing any number of COM interfaces (including the mandatory IUnknown). The coclasses are then packaged into a binary home, which is physically represented as a DLL or EXE file. Once all the (known) bugs have been squashed out of the code, the COM binary eventually ends up on some user's computer, ready to be accessed by other programs. The versioning problem in COM revolves around the fact that the COM runtime offers no intrinsic support to enforce that the correct version of a binary server is loaded for the calling client. It is true that a COM programmer can modify the version of the type library, update the registry to reflect these changes, and even reengineer the client's code base to reference a particular library. But, the fact remains that these are tasks delegated to the programmer and typically require rebuilding the code base. As many of you have learned the hard way, this is far from ideal. Assume that you have jumped through the necessary hoops to try to ensure the COM client activates the correct version of a COM binary. Your worries are far from over given that some other application may be installed on the target machine that overrides your carefully configured registry entries (and maybe even replaces a COM server or two with an earlier version during the process). Mysteriously, your client application may now fail to operate. For example, if you have 10 applications that all require the use of MyCOMServer.dll version 1.4, and another application installs MyCOMServer.dll version 2.0, all 10 applications are at risk of breaking. This is because we cannot be assured of complete backward compatibility. In a perfect world, all versions of a given COM binary are fully compatible with previous versions. In practice however, keeping COM servers (and software in general) completely backward compatible is extremely difficult. The lump sum of each of these versioning issues is lovingly referred to as 'DLL Hell' (which, by the way, is not limited to COM DLLs; traditional C DLLs suffer the same hellish existence). As you'll see during the course of this chapter, the .NET framework solves this nightmare by using a number of techniques including side-by-side execution and a very robust (yet very simple) versioning scheme. In a nutshell, .NET allows multiple versions of the same binary to be installed on the same target machine. Therefore, under .NET, if client A requires MyDotNETServer.dll version 1.4 and client B demands MyDotNETServer.dll version 2.0, the correct version is loaded for the respective client automatically. You are also able to bind to a specific version using an application configuration file. Problem: COM DeploymentThe COM runtime is a rather temperamental service. When a COM client wishes to make use of a coclass, the first step is to load the COM libraries for use by a given thread by calling CoInitialize(). At this point, the client makes additional calls to the COM runtime (e.g., CoCreateInstance(), CoGetClassObject() and so forth) to load a given binary into memory. The end result is that the COM client receives an interface reference that is then used to manipulate the contained coclass. In order for the COM runtime to locate and load a binary, the COM server must be configured correctly on the target machine. From a high level, registering a COM server sounds so simple: Build an installation program (or make use of a system supplied registration tool) to trigger the correct logic in the COM binary (DllRegisterServer() for DLLs or WinMain() for EXEs) and call it a day. However, as you may know, a COM server requires a vast number of registration entries to be made. Typically, every COM class (CLSID), interface (IID), library (LIBID), and application (AppID) must be inserted into the system registry. The key point to keep in mind is that the relationship between the binary image and the correct registry entries is extremely loose, and therefore extremely fragile. In COM, the location of the binary image (e.g., MyServer.dll) is entirely separate from the massive number of registry entries that completely describe the component. Therefore, if the end user were to relocate (or rename) a COM server, the entire system breaks, as the registration entries are now out of sync. The fact that classic COM servers require a number of external registration details also introduces another deployment difficulty: The same entries must be made on every machine referencing the server. Thus, if you have installed your COM binary on a remote machine, and if you have 100 client machines accessing this COM server, this means 101 machines must be configured correctly. To say the least, this is a massive pain in the neck. The .NET platform makes the process of deploying an application extremely simple given the fact that .NET binaries (i.e., assemblies) are not registered in the system registry at all. Plain and simple. Instead, assemblies are completely self-describing entities. Deploying a .NET application can be (and most often is) as simple as copying the files that compose the application to some location on the machine, and running your program.. In short, be prepared to bid a fond farewell to HKEY_CLASSES_ROOT. An Overview of .NET AssembliesNow that you understand the problem, let's check out the solution. .NET applications are constructed by piecing together any number of assemblies. Simply put, an assembly is nothing more than a versioned, self-describing binary (DLL or EXE) containing some collection of types (classes, interfaces, structures, etc.) and optional recourses (images, string tables and whatnot). One thing to be painfully aware of right now, is that the internal organization of a .NET assembly is nothing like the internal organization of a classic COM server (regardless of the shared file extensions). For example, an in-process COM server exports four functions (DllCanUnloadNow(), DllGetClassObject(), DllRegisterServer() and DllUnregisterServer()), in order to allow the COM runtime to access its contents. .NET DLLs on the other hand require only one function export, DllMain(). Local COM servers define WinMain() as the sole entry point into the EXE, which is implemented to test for various command line parameters to perform the same duties as a COM DLL. Not so under the .NET protocol. Although .NET EXE binaries do provide a WinMain() entry point (or main() for console applications), the behind-the-scenes logic is entirely different. The physical format of a .NET binary is actually more similar to a traditional portable executable (PE) and COFF (Common Object File Format) file formats. The true difference is that a traditional PE / COFF files contains instructions that target a specific platform and specific CPU. In contrast, .NET binaries contain code constructed using Microsoft Intermediate Language (MSIL or simply IL), which is platform- and CPU-agnostic. At runtime, the internal IL is compiled on the fly (using a just-in-time compiler) to platform and CPU specific instructions. This is a powerful extension of classic COM in that .NET assemblies are poised to be platform neutral entities that are not necessarily tied to the Windows operating system. In addition to raw IL, recall that an assembly also contains metadata that completely describes each type living in the assembly, as well as the full set of members supported by each type. For example, if you created a class named JoyStick using some .NET aware language, the corresponding compiler emits metadata describing all the fields, methods, properties, and events defined by this custom type. The .NET runtime uses this metadata to resolve the location of types (and their members) within the binary, create object instances, as well as to facilitate remote method invocations. Unlike traditional file formats or classic COM server, an assembly must contain an associated manifest (also referred to as 'assembly metadata'). The manifest documents each module within the assembly, establishes the version of the assembly, and also documents any external assemblies referenced by the current assembly (unlike a classic COM type library that does not document required external dependencies). Given this, a .NET assembly is completely self-describing. Single File and Multifile AssembliesUnder the hood, a given assembly can be composed of multiple modules. A module is really nothing more than a generic name for a valid file. In this light, an assembly can be viewed as a unit of deployment (often termed a 'logical DLL'). In many situations, an assembly is in fact composed of a single module. In this case, there is a one-to-one correspondence between the (logical) assembly and the underlying (physical) binary, as shown in Figure 6-1. When you create an assembly that is composed of multiple files, you gain efficient code download. For example, assume you have a remote client that is referencing a multifile assembly composed of three modules. If the remote application references only one of these modules, the .NET runtime only downloads the currently referenced file. If each module is 1 MB in size, I'm sure you can see the benefits. Understand that multifile assemblies are not literally linked together into a new (larger) file. Rather, multifile assemblies are logically related by information contained in the corresponding manifest. On a related note, multifile assemblies contain a single manifest that may be placed in a standalone file, but is more typically bundled into one of the related modules. The big picture is seen in Figure 6-2. This text is not concerned with the construction of multifile assemblies. However, be aware that online Help does document the process (which boils down to little more than passing the /addmodule flag to the C# compiler). Two Views of an Assembly: Physical and LogicalAs you begin to work with .NET binaries, it can be helpful to regard an assembly (both single file and multifile) as having two conceptual views. When you build an assembly, you are interested in the physical view. In this case, the assembly can be realized as some number of files that contain your custom types and resources (Figure 6-3). As an assembly consumer, you are interested in a logical view of the assembly (Figure 6-4). In this case, you can understand an assembly as a versioned collection of public types that you can use in your current application (recall that 'internal' types can only be referenced by the assembly in which they are defined): For example, the kind folks in Redmond who developed System.Drawing.dll created a physical assembly for you to consume in your applications. However, although System.Drawing.dll can be physically viewed as a binary DLL, you logically regard this assembly as a collection of related types. Of course, ILDasm.exe is the tool of choice when you are interested in discovering the logical layout of a given assembly (Figure 6-5). The chances are good that you will play the role of both an assembly builder and assembly consumer, as is the case throughout this book. However, before digging into the code, let's briefly examine some of the core benefits of this new file format. Assemblies Promote Code ReuseAssemblies contain code that is executed by the .NET runtime. As you might imagine, the types and resources contained within an assembly can be shared and reused by multiple applications, much like a traditional COM binary. Unlike COM, it is possible to configure 'private' assemblies as well (in fact, this is the default behavior). Private assemblies are intended to be used only by a single application on a given machine. As you will see, private assemblies greatly simplify the deployment and versioning of your applications. Like COM, binary reuse under the .NET platform honors the ideal of language independence. C# is one of numerous languages capable of building managed code, with even more languages to come. When a .NET-aware language adheres to the rules of the Common Language Specification (CLS), your choice of language becomes little more than a personal preference. Therefore, it is not only possible to reuse types between languages, but to extend types across languages as well. In classic COM, developers were unable to derive COM object A from COM object B (even if both types were developed in the same language). In short, classic COM did not support classical inheritance (the 'is-a' relationship). Later in this chapter you see an example of cross-language inheritance. Assemblies Establish a Type BoundaryAssemblies are used to define a boundary for the types (and resources) they contain. In .NET, the identity of a given type is defined (in part) by the assembly in which it resides. Therefore, if two assemblies each define an identically named type (class, structure, or whatnot) they are considered independent entities in the .NET universe. Assemblies Are Versionable and Self-Describing EntitiesAs mentioned, in the world of COM, the developer is in charge of correctly versioning a binary. For example, to ensure binary compatibility between MyComServer.dll version 1.0 and MyComServer.dll version 2.4, the programmer must use basic common sense to ensure interface definitions remained unaltered or run the risk of breaking client code. While a healthy dose of versioning common sense also comes in handy under the .NET universe, the problem with the COM versioning scheme is that these programmer-defined techniques are not enforced by the runtime. Another major headache with current versioning practices is that COM does not provide a way for a binary server to explicitly list the set of other binaries that must be present for it to function correctly. If an end user mistakenly moves, renames, or deletes a dependency, the solution fails. Under .NET, an assembly's manifest is the entity in charge of explicitly listing all internal and external contingencies. Each assembly has a version identifier that applies to all types and all resources contained within each module of the assembly. Using a version identifier the runtime is able to ensure that the correct assembly is loaded on behalf of the calling client, using a well defined versioning policy (detailed later). An assembly's version identifier is composed of two basic pieces: A friendly text string (termed the informational version) and a numerical identifier (termed the compatibility version). For example, assume you have created a new assembly with an informational string of 'MyInterestingTypes.' This same assembly would also define a compatibility number, such as 1.0.70.3. The compatibility version number always takes the same general format (four numbers separated by periods). The first and second numbers identify the major and minor version of the assembly (1.0 in this case). The third value (70) marks the build number, followed by the current revision number (3). As you discover later in this chapter, the .NET runtime makes use of an assembly's version to ensure the correct binary is loaded on behalf of the client (provided that the assembly is shared). Because the manifest explicitly lists all external dependencies, the runtime is able to determine the 'last known good' configuration (i.e., the set of versioned assemblies that are known to function correctly). Assemblies Define a Security ContextAn assembly may also contain security information. Under the architecture of the .NET runtime, security measures are scoped at the assembly level. For example, if AssemblyA wishes to use a class contained within AssemblyB, AssemblyB is the entity that chooses to provide access (or not). The security constraints defined by an assembly are explicitly listed within its manifest. While a treatment of .NET security measures is outside the mission of this text, simply be aware that access to an assembly's contents is verified using assembly metadata. Assemblies Enable Side-by-Side ExecutionPerhaps the biggest advantage of the .NET assembly is the ability of multiple versions of the same assembly to be loaded (and understood) by the runtime. Thus, it is possible to install and load multiple versions of the same assembly on a single machine. In this way, clients are isolated from other incompatible versions of the same assembly. Furthermore, it is possible to control which version of a (shared) assembly should be loaded using application configuration files. These files are little more than a simple text file describing (via XML syntax) the version, and specific location, of the assembly to be loaded on behalf of the calling application. You learn how to author application configuration files later in this chapter. Building a Single File Test AssemblyNow that you have a better understanding of .NET assemblies, let's build a minimal and complete code library using C#. Physically, this will be a single file assembly named CarLibrary. To build a code library using the Visual Studio.NET IDE, you would select a new Class Library project workspace (Figure 6-6). The design of our automobile library begins with an abstract base class named Car that defines a number of protected data members exposed through custom properties. This class has a single abstract method named TurboBoost() and makes use of a single enumeration (EngineState). Here is the initial definition of the CarLibrary namespace: // Our first code library (CarLibrary.dll) namespace CarLibrary { using System; public enum EngineState // Holds the state of the engine. { engineAlive, engineDead } public abstract class Car // The abstract base class in the hierarchy. { // Protected state data. protected string petName; protected short currSpeed; protected short maxSpeed; protected EngineState egnState; public Car(){egnState = EngineState.engineAlive;} public Car(string name, short max, short curr) { egnState = EngineState.engineAlive; petName = name; maxSpeed = max; currSpeed = curr; } public string PetName { get { return petName; } set { petName = value; } } public short CurrSpeed { get { return currSpeed; } set { currSpeed = value; } } public short MaxSpeed { get { return maxSpeed; } } public EngineState EngineState { get { return egnState; } } public abstract void TurboBoost(); } } Now assume that you have two direct descendents of the Car type named MiniVan and SportsCar. Each implements the abstract TurboBoost() method in an appropriate manner: namespace CarLibrary { using System; using System.Windows.Forms; // Needed for MessageBox definition. // The SportsCar public class SportsCar : Car { // Ctors. public SportsCar(){} public SportsCar(string name, short max, short curr) : base (name, max, curr){} // TurboBoost impl. public override void TurboBoost() { MessageBox.Show("Ramming speed!", "Faster is better. . ."); } } // The MiniVan public class MiniVan : Car { // Ctors. public MiniVan(){} public MiniVan(string name, short max, short curr) : base (name, max, curr){} // TurboBoost impl. public override void TurboBoost() { // Minivans have poor turbo capabilities! egnState = EngineState.engineDead; MessageBox.Show("Time to call AAA", "Your car is dead"); } } } Notice how each subclass implements TurboBoost() using the MessageBox class, which is defined in the System.Windows.Forms.dll assembly. In order for your assembly to make use of the types defined within this assembly, the CarLibrary project must include a reference to this binary using the 'Project | Add Reference' menu selection (Figure 6-7). In Chapter 8, the System.Windows.Forms namespace is described in detail. As you can tell by the name of the namespace, this assembly contains numerous types to help you build GUI applications. For now, the MessageBox class is all you need to concern yourself with. If you are following along, go ahead and compile your new code library. A C# Client ApplicationBecause each of our automobiles has been declared 'public,' other binaries are able to use our custom classes. In a moment, you learn how to make use of these types from other .NET aware languages such as Visual Basic. Until then, let's create a C# client. Begin by creating a new C# Console Application project (CSharpCarClient). Next, set a reference to your CarLibrary.dll, using the Browse button to navigate to the location of your custom assembly (again using the Add Reference dialog). Once you add a reference to your CarLibrary assembly, the Visual Studio.NET IDE responds by making a full copy of the referenced assembly and placing it into your Debug folder (assuming, of course, you have configured a debug build) (Figure 6-8). Obviously this is a huge change from classic COM, where the resolution of the binary is achieved using the system registry. Now that our client application has been configured to reference the CarLibrary assembly, you are free to create a class that makes use of these types. Here is a test drive (pun intended): // Our first taste of binary reuse. namespace CSharpCarClient { using System; // Make use of the CarLib types! using CarLibrary; public class CarClient { public static int Main(string[] args) { // Make a sports car. SportsCar viper = new SportsCar("Viper", 240, 40); viper.TurboBoost(); // Make a minivan. MiniVan mv = new MiniVan(); mv.TurboBoost(); return 0; } } } This code looks just like the other applications developed thus far. The only point of interest is that the C# client application is now making use of types defined within a unique assembly. Go ahead and run your program. As you would expect, the execution of this program results in the display of two message boxes. A Visual Basic.NET Client ApplicationWhen you install Visual Studio.NET, you receive four languages that are capable of building managed code: JScript.NET, C++ with managed extensions (MC++), C# and Visual Basic.NET. A nice feature of Visual Studio.NET is that all languages share the same IDE. Therefore, Visual Basic.NET, ATL, C#, and MFC programmers all make use of a common development environment. Given this fact, the process of building a Visual Basic.NET application making use of the CarLibrary is simple. Assume a new VB.NET Windows Application project workspace named VBCarClient (Figure 6-9) has been created. Similar to Visual Basic 6.0, this project workspace provides a design time template used to build the GUI of the main window. However, VB.NET is a completely different animal. The template you are looking at is actually a subclass of the Form type, which is quite different from a VB 6.0 Form object (more details in Chaper 8). Now, set a reference to the C# CarLibrary, again using the Add Reference dialog. Like C#, VB.NET requires you to list each namespace used within your project. However, VB.NET makes use of the 'imports' keyword rather than the C# 'using' directive. Thus, open the code window for your Form and add the following: ' Like C#, VB.NET needs to 'see' the namespaces used by a given class.
Imports System
Imports System.Collections
. . .
Imports CarLibrary
Using the design time template, construct a minimal and complete user interface to exercise your automobile types (Figure 6-10). Two buttons should fit the bill (simply select the Button widget from the Toolbox and draw it on the Form object). The next step is to add event handlers to capture the Click event of each Button object. To do so, simply double-click each button on the Form. The IDE responds by writing stub code that will be called when a button is clicked. Here is some sample code: ' A little bit of VB.NET!
Protected Sub btnMiniVan_Click(ByVal sender As Object,
ByVal e As System.EventArgs) Handles btnMiniVan.Click
Dim sc As New MiniVan()
sc.TurboBoost()
End Sub
Protected Sub btnCar_Click(ByVal sender As Object,
ByVal e As System.EventArgs) Handles btnCar.Click
Dim sc As New SportsCar()
sc.TurboBoost()
End Sub
Although the goal of this book is not to turn you into a powerhouse VB.NET developer, here is one point of interest. Notice how each Car subclass is created using the New keyword. Unlike VB 6.0 however, classes now have true constructors! Therefore, the empty parentheses suffixed on the class name do indeed invoke a given constructor on the class. As you would expect, when you run the program, each automobile responds appropriately. Cross-Language InheritanceA very sexy aspect of .NET development is the notion of cross-language inheritance. To illustrate, let's create a new VB.NET class that derives from CarLibrary.SportsCar. Impossible you say? Well, if you were using Visual Basic 6.0 this would be the case. However with the advent of VB.NET, programmers are able to use the same object-oriented features found in C#, Java and C++, including classical inheritance (i.e., the 'is-a' relationship). To illustrate, add a new class named PerformanceCar to your current VB.NET client application (using the 'Project | Add Class' menu selection). In the code that follows, notice you are deriving from the C# Car type using the VB.NET 'Inherits' keyword. As you recall, the Car class defined an abstract TurboBoost() method, which we implement using the VB.NET 'Overrides' keyword: ' Yes, VB.NET supports each pillar of OOP!
Imports CarLibrary
Imports System.Windows.Forms
' This VB type is deriving from the C# SportsCar!
Public Class PerformanceCar
Inherits CarLibrary.SportsCar
' Implementation of abstract Car method.
Overrides Sub TurboBoost()
MessageBox.Show("Blistering speed", "VB PerformanceCar says")
End Sub
End Class
If we update our existing Form to include an additional Button to exercise the performance car, we could write the following test code: Protected Sub btnPreCar_Click(ByVal sender As Object,
ByVal e As System.EventArgs) Handles btnPerfCar.Click
Dim pc As New PerformanceCar()
pc.PetName = "Hank" ' Inherited property.
' Display base class.
MessageBox.Show(pc.GetType().BaseType.ToString(), "Base class of Perf car")
' Custom Implementation of Car.TurboBoost()
pc.TurboBoost()
End Sub
Notice that we are able to identify our base class programmatically (Figure 6-11). Excellent! At this point you have begun the process of breaking your applications into discrete binary building blocks. Given the language-independent nature of .NET, any language targeting the runtime is able to create (and extend) the types described within a given assembly. Exploring the CarLibrary's ManifestAt this point, you have successfully created a single file assembly and two client applications. Your next order of business is to gain a deeper understanding of how .NET assemblies are constructed under the hood. To begin, recall that every assembly contains an associated manifest, which can be regarded as the Rosetta stone of .NET. A manifest contains metadata that specifies the name and version of the assembly, as well as a listing of all internal and external modules that compose the assembly as a whole. Additionally, a manifest may contain culture information (used for internalization), a corresponding 'strong name' (required by shared assemblies) and optional security and resource information (we will examine the .NET resource format in Chapter 10). .NET aware compilers (such as csc.exe) automatically create a manifest at compile time. As you see in Chapter 7, it is possible to augment the compiler-generated manifest using attribute-based programming techniques. For now, go ahead and load the CarLibrary assembly into ILDasm.exe. As you can see, this tool has read the metadata to display relevant information for each type (Figure 6-12). Now, open the manifest by double clicking on the MANIFEST icon (Figure 6-13). The first code block contained in a manifest is used to specify all external assemblies that are required by the current assembly to function correctly. As you recall, CarLibrary.dll made use of mscorlib.dll and System.Windows.Forms.dll, each of which are marked in the manifest using the [.assembly extern] tag: .assembly extern mscorlib
{
.publickeytoken = (B7 7A 5C 56 19 34 E0 89 ) // .z\V.4
.ver 1:0:2411:0
}
.assembly extern System.Windows.Forms
{
.publickeytoken = (B7 7A 5C 56 19 34 E0 89 ) // .z\V.4..
.ver 1:0:2411:0
}
Here, each [.assembly extern] block is colored by the [.publickeytoken] and [.ver] directives. The [.publickeytoken] instruction is only present if the assembly has been configured as a shared assembly and is used to reference the 'strong name' of the shared assembly (more details later). [.ver] is (of course) the numerical version identifier. After enumerating each of the external references, the manifest then enumerates each module contained in the assembly. Given that the CarLibrary is a single file assembly, you will find exactly one [.module] tag. This manifest also lists a number of attributes (marked with the [.custom] tag) such as company name, trademark and so forth, all of which are currently empty (more information on these attributes in Chapter 7): .assembly CarLibrary
{
.custom instance void [mscorlib]
System.Reflection.AssemblyKeyNameAttribute::.ctor(string) = ( 01 00 00 00 00 )
.custom instance void [mscorlib]
System.Reflection.AssemblyKeyFileAttribute::.ctor(string) = ( 01 00 00 00 00 )
.custom instance void [mscorlib]
System.Reflection.AssemblyDelaySignAttribute::.ctor(bool) = ( 01 00 00 00 00 )
.custom instance void [mscorlib]
System.Reflection.AssemblyTrademarkAttribute::.ctor(string) = ( 01 00 00 00 00 )
.custom instance void [mscorlib]
System.Reflection.AssemblyCopyrightAttribute::.ctor(string) = ( 01 00 00 00 00 )
.custom instance void [mscorlib]
System.Reflection.AssemblyProductAttribute::.ctor(string) = ( 01 00 00 00 00 )
.custom instance void [mscorlib]
System.Reflection.AssemblyCompanyAttribute::.ctor(string) = ( 01 00 00 00 00 )
.custom instance void [mscorlib]
System.Reflection.AssemblyConfigurationAttribute::.ctor(string)=( 01 00 00 00 00 )
.custom instance void [mscorlib]
System.Reflection.AssemblyDescriptionAttribute::.ctor(string) = ( 01 00 00 00 00 )
.custom instance void [mscorlib]
System.Reflection.AssemblyTitleAttribute::.ctor(string) = ( 01 00 00 00 00 )
.hash algorithm 0x00008004
.ver 1:0:454:30104
}
.module CarLibrary.dll
Here, you can see that the [.assembly] tag is used to mark the friendly name of your custom assembly (CarLibrary). Like external declarations, the [.ver] tag defines the compatibility version number for this assembly, where [.hash] marks the file's generated hash code. Do note that the CarLibrary assembly does not define an [.publickeytoken] tag, given that CarLibrary has not been configured as a shared assembly. To summarize the tags that dwell in the assembly manifest, ponder Table 6-1. Table 6-1. Manifest IL Tags
Exploring the CarLibrary's TypesRecall that an assembly does not contain platform specific instructions, but rather platform agnostic intermediate language (IL). When the .NET runtime loads an assembly into memory, the underlying IL is compiled (using the JIT compiler) into instructions that can be understood by the target platform. Also recall that in addition to raw IL and the assembly manifest, an assembly contains metadata that describes and members of each type contained within a given module. For example, if you were to double click the TurboBoost() method of the SportsCar class, ILDasm.exe would open a new window showing the raw IL instructions. Notice in the following screen shot, that the [.method] tag is used to identify (of course) a method defined by the SportsCar type (Figure 6-14). As you might expect, public data defined by a type is marked with the [.field] tag (Figure 6-15). Recall that the Car class defined a set of protected data, such as currSpeed (note that the 'family' tag signifies protected data). Properties are also marked with the [.property] tag (Figure 6-16). The figure shows the IL describing the public property that provides access to the underlying currSpeed data point (note the read/write nature of the CurrSpeed property is marked by .get and .set tags): If you now select the 'Ctrl + M' keystroke, ILDasm.exe would display the metadata for each type (Figure 6-17). Using this metadata, the .NET runtime is able to locate and construct object instances, and invoke methods. Various tools (such as Visual Studio.NET) make use of metadata at design time in order to validate the number of (and type of) parameters during compilation. To summarize the story so far, make sure the following points are clear in your mind:
Next you need to distinguish between private and shared assemblies. If you are coming into the .NET paradigm from a classic COM perspective, be prepared for some significant changes. Understanding Private AssembliesFormally speaking, an assembly is either 'private' or 'shared.' The good news is each variation has the same underlying structure (i.e., some number of modules and an associated manifest). Furthermore, each flavor of assembly provides the same kind of services (for example, access to some number of public types). The real differences between a private and shared assembly boils down to naming conventions, versioning policies, and deployment issues. Let's begin by examining the traits of a private assembly, which is far and away the most common of the two options. Private assemblies are a collection of types that are only used by the application with which it has been deployed. For example, CarLibrary.dll is a private assembly used by the CSharpCarClient and VBCarClient applications. When you create a private assembly, the assumption is that the collection of types are only used by the 'owning' application, and not shared with other applications on the system. Private assemblies are required to be located within the main directory of the owning application (termed the application directory) or a subdirectory thereof. For example, recall that when you set a reference to the CarLibrary assembly (as we did in the CSharpCarClient and VBCarClient applications), the Visual Studio.NET IDE responded by making a full copy of the assembly that was placed it in your project's application directory. This is the default behavior, as private assemblies are assumed to be deployment option of choice. Note the painfully stark contrast to classic COM. There is no need to register any items under HKEY_CLASSES_ROOT and no need to enter a hard-coded path to the binary using an InprocServer32 or LocalServer32 listing. The resolution and loading of the private CarLibrary happens by virtue of the fact that the assembly is placed in the application directory. In fact, if you moved CSharpCarClient.exe and CarLibrary.dll to a new directory, the application would still run. To illustrate this point, copy these two files to your desktop and run the client (Figure 6-18). Uninstalling (or replicating) an application that makes exclusive use of private assemblies is a no-brainer. Delete (or copy) the application folder. Unlike classic COM, you do not need to worry about dozens of orphaned registry settings. More important, you do not need to worry that the removal of private assemblies will break any other applications on the machine! Probing BasicsLater in this chapter, you are exposed to a number of gory details regarding location resolution of an assembly. Until then, the following overview should help prime the pump. Formally speaking, the .NET runtime resolves the location of a private assembly using a technique termed probing, which is much less invasive than it sounds. Probing is the process of mapping an external assembly reference (i.e., [.assembly extern]) to the correct corresponding binary file. For example, when the runtime reads the following line from the VBCarClient's manifest: .assembly extern CarLibrary
{
. . .
}
a search is made in the application directory for a file named CarLibrary.DLL. If a DLL binary cannot be located, an attempt is made to locate an EXE version (CarLibrary.EXE). If neither of these files can be found, a further examination ensues for a shared assembly (examined in just a bit). The Identity of a Private AssemblyThe identity of a private assembly consists of a friendly string name and numerical version, both of which are recorded in the assembly manifest. The friendly name is created based on the name of the binary module that contains the assembly's manifest. For example, if you examine the manifest of the CarLibrary.dll assembly, you find the following (the exact version may vary): .assembly CarLibrary as "CarLibrary"
{
. . .
.ver 1:0:454:30104
}
However, given the nature of a private assembly, it should make sense that the .NET runtime does not bother to apply any version policies when loading the assembly. The assumption is that private assemblies do not need to have any elaborate version checking, given that the client application is the only entity that "knows" of its existence. As an interesting corollary you should understand that it is (very) possible for a single machine to have multiple copies of the same private assembly in various application directories. Private Assemblies and XML Configuration FilesWhen the .NET runtime is instructed to bind to an assembly, the first step is to determine the presence of an application configuration file. These optional files contain XML tags that control the binding behavior of the launching application. By law, configuration files must have the same name as the launching application and take a *.config file extension. As mentioned, configuration files can be used to specify any optional subdirectories to be searched during the process of binding to private assemblies. As you have seen earlier in this chapter, a componentized .NET application can be deployed simply by placing all assemblies into the same directory as the launching application. Often, however, you may wish to deploy an application such that the application directory contains a number of related subdirectories, in order to give some meaningful structure to the application as a whole. You see this all the time in commercial software. For example, assume our main directory is called MyRadApplication, which contains a number of subdirectories (\Images, \Bin, \SavedGames, \OtherCoolStuff). Using application configuration files, you can instruct the runtime where it should probe while attempting to locate the set of private assemblies used by the launching application. To illustrate, let's create a simple configuration file for the previous CSharpCarClient application. Our goal is to move the referenced assembly (CarLibrary) from the Debug folder into a new subdirectory named Foo \ Bar. Go ahead and move this file now (Figure 6-19). Now, create a new configuration file named CSharpCarClient.exe.config (notepad will do just fine) and save it into the same folder containing the CSharpCarClient.exe application. The beginning of an application configuration file is marked with the <Configuration. tag. Before the closing </Configuration. tag, specify an assemblyBinding row, which is used to specify alternative locations to search for a given assembly, using the privatePath attribute (FYI, multiple subdirectories can be specified using a semicolon delimited list): <configuration>
<runtime>
<assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">
<probing privatePath="foo\bar"/>
</assemblyBinding>
</runtime>
</configuration>
Once you are done, save the file and launch the client. You will find that the CSharpCarClient application runs without a hitch. As a final test, change the name of your configuration file and attempt to run the program once again (Figure 6-20). The client application silently fails. Recall that configuration files must have the same name as the launching application. Because you have renamed this file, the .NET runtime assumes you do not have a configuration file, and thus attempts to probe for the referenced assembly in the application directory (which it cannot locate). Specifics of Binding to a Private AssemblyTo wrap up the current discussion, let's formalize the specific steps involved in binding to a private assembly at runtime. First, a request to load an assembly may be either explicit or implicit. An implicit load request occurs whenever the manifest makes a direct reference to some external assembly. As you recall, external references are marked with the [.assembly extern] instruction: // An implicit load request. . .
.assembly extern CarLibrary
{
. . .
}
An explicit load request occurs programmatically using System.Reflection.Assembly.Load().The Assembly class is examined in Chapter 7, but be aware that the Load() method allows you to specify the name, version, strong name, and culture information syntactically (note you are not required to specify each characteristic): // An explicit load request. . .
Assembly asm = Assembly.Load("CarLibrary");
Collectively, the name, version, strong name, and culture information is termed an assembly reference (or simply AsmRef). The entity in charge of locating the correct assembly based on an AsmRef is termed the assembly resolver, which is a facility of the CLR. As mentioned earlier, an application directory is nothing more than a folder on your hard drive (for example, C:\MyApp) that contains all the files for a given application. If necessary, an application directory may specify additional subdirectories (e.g., C:\MyApp\Bin, C:\MyApp\Tools, and so on) to establish a more stringent file hierarchy. When a binding request is made, the runtime passes an AsmRef to the assembly resolver. If the resolver determines the AsmRef refers to a private assembly (meaning there is no strong name recorded in the manifest), the following steps are followed:
To solidify this sequence of events, Figure 6-21 illustrates the process outlined above. Again, as you can see, the location of a private assembly is fairly simply to resolve. If the application directory does not contain a configuration file, the assembly resolver simply looks for a binary that matches the correct string name. If the application directory does contain a configuration file, any specified subdirectories are also searched. Understanding Shared AssembliesLike a private assembly, a "shared" assembly is a collection of types and (optional) resources contained within some number of modules. The most obvious difference between shared and private assemblies is the fact that shared assemblies can be used by several clients on a single machine. Clearly, if you wish to create a machine-wide class library, a shared assembly is the way to go. A shared assembly is typically not deployed within the same directory as the application making use of it. Rather, shared assemblies are installed into a machine-wide Global Assembly Cache, which lends itself to yet another colorful acronym in the programming universe: the GAC. The GAC itself is located under the <drive.: \ WinNT \ Assembly subdirectory (Figure 6-22). This is yet another major difference between the COM and .NET architectures. In COM, shared applications can reside anywhere on a given machine, provided they are properly registered. Under .NET, shared assemblies are typically placed into a centralized well-known location (the GAC). Unlike private assemblies, a shared assembly requires additional version information beyond the friendly text string. As you may have guessed, the .NET runtime does enforce version checking for a shared assembly before it is loaded on behalf of the calling application. In addition, a shared assembly must be assigned a "shared name" (also known as a "strong name"). Problems with Your GAC?By way of a quick side note, as of Beta2 (on which this text is based) I have noticed that some of my development machines are unable to display the GAC correctly. The problem is that the GAC is a shell extension that requires the registration of a COM server named shfusion.dll. During installation, this server may fail to register correctly. If you are having problems opening the GAC on your machine, simply register this COM server using regsvr32.exe and you should be just fine. Understanding Shared (Strong) NamesWhen you wish to create an assembly that can be used by numerous applications on a given machine, your first step is to create a unique shared name for the assembly. A shared name contains the following information:
The composition of a shared name is based on standard public key cryptography. When you create a shared assembly, you must generate a public/private key pair (that you do momentarily). The key pair is included in the build cycle using a .NET aware compiler, which in turn lists a token of the public key in the assembly's manifest (via the [.publickeytoken] tag). The private key is not listed in the manifest, but rather, is signed with the public key. The resulting signature is stored in the assembly itself (in the case of a multifile assembly, the private key is stored with the file defining the manifest). Now, assume some client has referenced this shared assembly (which is no different than referencing a private assembly). When the compiler generates the client binary, the public key is recorded in its manifest. At runtime, the .NET runtime ensures that both the client and the shared assembly are making using of the same key pair. If these keys are identical, the client application can rest assured that the correct assembly as been loaded. Figure 6-23 presents the basic picture. As you might guess, there are additional details regarding key pairs. We really don't need more details for now, so check out online Help if you so choose. Building a Shared AssemblyTo generate a strong name for your assembly, you need to make use of the sn.exe (strong name) utility. Although this tool has numerous command line options, all we need to concern ourselves with is the "-k" argument, which instructs the tool to generate a new strong name key that will be saved to a specified file (Figure 6-24). If you examine the contents of this new file (theKey.snk) you see the binary markings of the key pair (Figure 6-25). To continue with the example, assume you have created a new C# Class Library called (of course) SharedAssembly, which contains the following class definition: using System; using System.Windows.Forms; namespace SharedAssembly { public class VWMiniVan { public VWMiniVan(){} public void Play60sTunes() { MessageBox.Show("What a loooong, strange trip it's been. . ."); } private bool isBustedByTheFuzz = false; public bool Busted { get { return isBustedByTheFuzz; } set { isBustedByTheFuzz = value; } } } } The next step is to record the public key in the assembly manifest. The easiest way to do so is to leverage the use of an attribute named AssemblyKeyFile. When you create a new C# project workspace, you will notice that one of your initial project files is named "AssemblyInfo.cs" (Figure 6-26). This file contains a number of (initially empty) attributes that are consumed by a .NET aware compiler. If you examine this file, you find one such attribute named AssemblyKeyFile. To specify the strong name for a shared assembly, simply update the initial empty value with a string specifying the location of your *.snk file: [assembly: AssemblyKeyFile(@"D:\SharedAssembly\theKey.snk")] Using this assembly level attribute, the C# compiler now merges the necessary information into the corresponding manifest, as can be seen using ILDasm.exe (note the [.publickey] tag in Figure 6-27). Installing Assemblies into the GACOnce you have established a strong name for your shared assembly, the final step is to install it into the GAC. The simplest approach to install a private assembly into the GAC is to drag and drop the file(s) onto the active window (you are also free to make use of the gacutil.exe command line utility). SeeFigure 6-28. Do be aware that you must have Administrative rights on the computer to install assemblies into the GAC. This is a good thing, in that it prevents the casual user from accidentally breaking existing applications. The end result is that your assembly has now been placed into the GAC, and may be shared by multiple applications on the target machine. On a related note, when you wish to remove an assembly from the GAC, you may do so with a simple right-click (just select Delete from the Context menu). Using a Shared AssemblyNow to prove the point, assume you have created a new C# Console application (called SharedAssemblyUser), set a reference to the SharedAssembly binary, and created the following class definition: namespace SharedLibUser { using System; using SharedAssembly; public class SharedAsmUser { public static int Main(string[] args) { try { VWMiniVan v = new VWMiniVan(); v.Play60sTunes(); } catch(TypeLoadException e) { // Can't find assembly! Console.WriteLine(e.Message); } return 0; } } } Recall, that when you reference a shared assembly, IDE automatically creates a local copy of the assembly for use by the client application. However, when you reference an assembly that contains a public key (as is the case with the SharedAssembly.dll), you do not receive a local copy. The assumption is that assemblies containing a public key are designed to be shared (and are therefore placed in the GAC). Do be aware that the VS.NET IDE allows you to explicitly control the copying of a given assembly using the Properties window. For example, if you have set a reference to the external binary, select this assembly using the Solution Explorer and set Copy Local to false. This will delete the local copy (Figure 6-29). Now run the client application once again. If all is well, everything should still function correctly, as the .NET runtime consulted the GAC during its quest to resolve the location of the requested assembly (Figure 6-30). Understanding .NET Version PoliciesAs you have already learned, the .NET runtime does not bother to perform version checks for private assemblies. The versioning story changes significantly when a request is made to load a shared assembly. Given that the version of a shared assembly is of prime importance, let's review the composition of version numbers. As you recall, a version number is marked by four discrete parts (for example 2.0.2.11). Logically however, the .NET runtime is able to extract three meaningful bits of information regarding version compatibility, as illustrated in Figure 6-31. Whenever two assemblies differ by either the major or minor version number (e.g., 2.0 vs. 2.5) they are considered to be completely incompatible with each other as far as the .NET runtime is concerned. When assemblies differ by major or minor numerical markings, you can assume significant changes have occurred (e.g., method name changes, types have been added or removed, parameters have changed, and so forth). Therefore, if a client is requesting a bind to version 2.0 but the GAC only contains version 2.5, the bind request fails (unless overridden by an application configuration file). If two assemblies have identical major and minor version numbers, but have different revision numbers (e.g., 2.5.0.0 vs. 2.5.1.0) the .NET runtime assumes they might be compatible with each other (in other words, backward compatibility is assumed, but not guaranteed). By way of a concrete example, a Service Pack release typically involves modifying the revision number of an assembly. Finally, you have the Quick Fix Engineering (QFE) number. When two assemblies differ only by their QFE value, the .NET runtime assumes they are fully compatible. QFEs are typically modified with the release of a software patch. The idea here is that all calling conventions (e.g., method names, parameters, supported interfaces, and so forth) are identical to previous versions. Recording Version InformationOne question you might be asking yourself at this point is where was this version number specified? Recall that every C# project defines a file named AssemblyInfo.cs. If you examine this file, you will see an attribute named AssemblyVersion, which is initially set to a string reading "1.0.*": [assembly: AssemblyVersion("1.0.*")]
Every new C# projects begins life versioned at 1.0. As you build new versions of a shared assembly, part of your task is to update the four-part version number for your shared assembly. Do be aware that the IDE automatically increments the build and revision numbers (as marked by the '*' tag). If you wish to enforce an application-specific value for the assembly's build and/or revision, simply update accordingly: [assembly: AssemblyVersion("1.0.0.0")]
Freezing the Current SharedAssemblyTo really understand .NET versioning policies, we need to have a concrete example. The current goal is to update your previous SharedAssembly.dll to support additional functionality, update the version number, and then place the new version into the GAC. At this point, you are able to experiment with the use of application configuration files to specify various version policies, as well as side-by-side execution. To begin, let's update the constructor of the VWMiniVan class to display a message verifying the current version: public VWMiniVan() { MessageBox.Show("Using version 1.0.0.0!", "Shared Car"); } Next, update the AssemblyVersion attribute to be fully qualified to version 1.0.0.0 (as seen in the previous section). Go ahead and recompile the project. The next thing you need to do is ensure that our original SharedAssembly.dll is removed from the GAC (go ahead and delete this assembly now). Next, move your existing 1.0.0.0 assembly into a new folder (I called mine Version1) to ensure you freeze this version (Figure 6-32). Now (once again!) place this assembly back into the GAC. Notice that the version of this assembly is <1.0.0.0. (Figure 6-33). Once version 1.0.0.0 of the SharedAssembly has been inserted into the GAC, right-click this assembly and select Properties from the context-sensitive pop-up menu. Verify that the path to this binary maps to the Version1 subdirectory. Finally, rebuild and run the current SharedAssemblyUser application. Things should continue to work just fine. Building SharedAssembly Version 2.0To illustrate the .NET version policy, let's modify the current SharedAssembly project. Update your VWMiniVan class with a new member (which makes use of a custom enumeration) to allow the user to play some more modern musical selections. Also be sure to update the message displayed from within the constructor logic. // Which band do you want? public enum BandName { TonesOnTail, SkinnyPuppy, deftones, PTP } public class VWMiniVan { public VWMiniVan() { MessageBox.Show("Using version 2.0.0.0!", "Shared Car"); } . . . public void CrankGoodTunes(BandName band) { switch(band) { case BandName.deftones: MessageBox.Show("So forget about me. . ."); break; case BandName.PTP: MessageBox.Show("Tick tick tock. . ."); break; case BandName.SkinnyPuppy: MessageBox.Show("Water vapor, to air. . ."); break; case BandName.TonesOnTail: MessageBox.Show("Oooooh the rain. Oh the rain."); break; default: break; } } } Before you compile, let's upgrade this version of this assembly to 2.0.0.0: // Update your assemblyinfo.cs file as so. . . [assembly: AssemblyVersion("2.0.0.0")] If you look in your project's debug folder, you see that you have a new version of this assembly (2.0) while the previous version is safe in storage under the Version1 directory. Finally, let's install this new assembly into the GAC. Notice that you now have two versions of the same assembly (Figure 6-34). Now that you have a distinctly versioned assembly recorded in the GAC, you can begin to work with application configuration files to control how a client binds to a given version. But first, a few words about the default binding policy. Understanding the Default Version PolicyAs mentioned earlier in the chapter, if a client is referencing a shared assembly, the major and minor versions must be identical if the bind is to succeed. However, the .NET runtime binds to a given assembly if the assembly reference differs by the revision or build numbers. This behavior is termed the default version policy and is used to ensure that a client always gets the latest and greatest service release (i.e., bug fix) of a given assembly. Thus, if the client's manifest explicitly requests version 1.0.0.0, but the GAC has a newer version by specifying a QFE (such as 1.0.2.2), the client automatically receives the most recent fix. In this way, a client application is guaranteed that the assembly that it is referencing is backward compatible, in addition to being as bug-free as possible. Specifying Custom Version PoliciesWhen you wish to dynamically control how an application binds to an assembly (such as disabling QFEs), you need to author an application configuration file. As you have already seen during the discussion of private assemblies, configuration files are blocks of XML that are used to customize the binding process. Recall that these files must have the same name as the owning application (with a *.config extension) and be placed directly in the application directory. In addition to the privatePath tag (used to specify where to probe for private assemblies), a configuration file may specify information for shared assemblies. The first point of interest is using an application configuration file to specify a specific assembly version that is to be loaded, regardless of what may be listed in the corresponding manifest. When you wish to redirect a client to bind to an alternate shared assembly, you make use of the <dependentAssembly. and <bindingRedirect. attributes. For example, the following configuration file forces version 2.0.0.0: <configuration>
<runtime>
<assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">
<dependentAssembly>
<assemblyIdentity name="sharedassembly"
publicKeyToken="6c0646f072c6fe39"
culture=""/>
<bindingRedirect oldVersion= "1.0.0.0"
newVersion= "2.0.0.0"/>
</dependentAssembly>
</assemblyBinding>
</runtime>
</configuration>
Here, the oldVersion tag is used to specify the version that you wish to override. The newVersion tag marks a specific version to load. To test this out yourself, create the previous configuration file and save it into the directory of the SharedAssemblyUser application (be sure you name this configuration file correctly). Now, run the program. You should see the message that appears in Figure 6-35. If you update the newVersion attribute to 1.0.0.0, you now see the message that appears in Figure 6-36. Way cool. What you have just observed is the notion of side-by-side execution mentioned earlier in the chapter. Because the .NET framework allows you to place multiple versions of the same assembly into the GAC, you can easily configure custom version policies as you (or a system administrator) see fit. As you have seen, the .NET framework does indeed take the version of a shared assembly seriously. Through the use of application configuration files, you are able to control a number of details regarding which version of a given assembly should be loaded by an owning application. As you may expect, there are additional attributes that may be listed in an application's configuration file. Investigate these details as you wish. However, there is one final aspect to consider. . . The Administrator Configuration FileThe configuration files you have been examining in this chapter each have a common theme. They only apply to a specific application (that is why they had the same name as the owning application). The .NET framework does allow an additional type of configuration file called the administrator configuration file. Each .NET-aware machine has a file named "machine.config" that contains listings used to override any application-specific configuration files. As you might guess, reading this file is a great way to learn more *.config centric tags. Now that you have an intimate understanding of .NET assemblies, let's switch gears completely and examine the related topics of application domains and multithreaded assemblies. Although this may seem like a drastic change of content, you will see that assemblies, application domains, and threads are interrelated. Review of Traditional Win32 Thread ProgrammingDepending on your programming background, you may be extremely interested in building multithreaded binaries, could care less about building multithreaded binaries, or are a little unsure what multithreading means in the first place. In order to level the playing field, let's take the time to quickly review the basics of multithreading. Once you have reviewed multithreading from a traditional Win32 perspective, you will then come to understand how things have changed under the .NET platform. To begin, recall that under traditional Win32, each application is hosted by a process. Understand that process is a generic term used to describe the set of external resources (such as a COM server) as well as the necessary memory allocations used by a given application. For each EXE loaded into memory, the operating system creates a separate and isolated memory partition (i.e., process) for use during its lifetime. Every running process has at least one main "thread" that serves as the entry point for the application. Formally speaking, the first thread created in a given process is termed the primary thread. Simply put, a thread is a specific path of execution within the Win32 process. A traditional Windows applications defines the WinMain() method to function as the application's entry point. On the other hand, Console application provides the main() method for the same purpose. Applications that contain only a single thread of execution are automatically "thread-safe" given the fact that there is only one thread that can access the data in the application at a given time. On the downside, a single-threaded application can appear a bit unresponsive to the end user if this single thread is performing a complex operation (such as printing out a lengthy text file, performing an exotic calculation, or connecting to a remote server). Under Win32, it is possible for the primary thread to spawn additional secondary threads in the background, using a handful of Win32 API functions such as CreateThread(). Each thread (primary or secondary) becomes a unique path of execution in the process and has concurrent access to all data in that process. As you may have guessed, developers typically create additional threads to help improve the program's overall responsiveness. Multithreaded applications provide the illusion that numerous activities are happening at more or less the same time. For example, you could spawn a background worker thread to perform a labor-intensive unit of work (again, such as printing a large text file). As this secondary thread is churning away, the main thread is still responsive to user input, which gives the entire process the potential of delivering greater performance. However, this is only a possibility. Too many threads in a single process can actually degrade performance, as the CPU must switch between the active threads in the process (which takes time). In reality, multithreading is often a simple illusion provided by the operating system. Machines that host a single CPU do not have the ability to literally handle multiple threads at the same exact time. Rather, a single CPU will execute one thread for a unit of time (called a time-slice) based on the thread's priority level. When a thread's time-slice is up, the existing thread is suspended to allow the other thread to perform its business. In order for a thread to remember what was happening before it was kicked out of the way, each thread is given the ability to write to Thread Local Storage (TLS) and is provided a separate call stack, as illustrated in Figure 6-37. Problem of Concurrency and Thread SynchronizationBeyond taking time, the process of switching between threads can cause additional problems. For example, assume a given thread is accessing a shared point of data, and in the process begins to modify it. Now assume that the first thread is told to wait, to allow another thread to access the same point of data. If the first thread was not finished with its task, the second thread may be modifying data that is in an unstable state. To protect the application's data from possible corruption, the Win32 developer must make use of any number of Win32 threading primitives such as critical sections, mutexes or semaphores to synchronize access to shared data. Given this, multithreaded applications are much more volatile, as numerous threads can operate on the application's data at the same time. Unless the developer has accounted for this possibility using threading primitives (such as a critical section) the program may end up with a good amount of data corruption. 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 comes time to lock down shared points of data, you will find additional types that provide the same functionality as the Win32 threading primitives. Understanding System.AppDomainBefore we examine the full details of the System.Threading namespace, we need to examine the concept of application domains. As you know, .NET applications are created by piecing together any number of related assemblies. However, unlike a traditional (non-.NET) Win32 EXE application, .NET applications are hosted by an entity termed an "application domain" (aka AppDomain). Be very aware that the term AppDomain is not a synonym for a Win32 process. In reality, a single process can host any number of AppDomains, each of which is fully and completely isolated from other AppDomains within this process (or any other process). Applications that run in different AppDomains are unable to share any information of any kind (global variables or static fields) unless they make use of the .NET remoting protocol. The big picture is shown in Figure 6-38. Notice the stark difference from a traditional Win32 process. Under .NET, a single process may contain multiple AppDomains. Each AppDomain may contain multiple threads. In some respects, this layout is reminiscent of the "apartment" architecture of classic COM. Of course, .NET AppDomains are managed types whereas the COM apartment architecture is built on an unmanaged (and much more complex) architecture. AppDomains are programmatically represented by the System.AppDomain type. Some core members to be aware of are shown in Table 6-2. Fun with AppDomainsAs you can see, the members of AppDomain provide numerous process-like behaviors, with a .NET flair. To illustrate some of this flair, consider the following namespace definition: namespace MyAppDomain { using System; using System.Windows.Forms; // Need this namespace to work with the Assembly type. using System.Reflection; public class MyAppDomain { public static void PrintAllAssemblies() { // Ask the current AppDomain for a list of all // loaded assemblies. AppDomain ad = AppDomain.CurrentDomain; Assembly[] loadedAssemblies = ad.GetAssemblies(); Console.WriteLine("Here are the assemblies loaded in " + "this appdomain\n"); // Now print the fully qualified name of each one. foreach(Assembly a in loadedAssemblies) { Console.WriteLine(a.FullName); } } public static int Main(string[] args) { // Force the loading of the Windows Forms assembly. MessageBox.Show("Loaded System.Windows.Forms.dll"); PrintAllAssemblies(); return 0; } } } First of all, notice that you are making use of a new namespace, System.Reflection. Full details of this namespace are seen in Chapter 7. For the time being, just understand that this namespace defines the Assembly type, which we need access to given the role of the PrintAllAssemblies() method. This static member obtains a reference to the hosting AppDomain, and enumerates over the list of loaded assemblies. To make it more interesting, notice that the Main() method launches a message box in order to force the assembly resolver to load the System.Windows.Forms.dll assembly (which in turn loads other referenced assemblies). Figure 6-39 shows the output. System.Threading NamespaceThe System.Threading namespace provides a number of types that enable multithreaded programming. In addition to providing types that represent a specific thread, this namespace also defines types that can manage a collection of threads (ThreadPool), a simple (non-GUI based) Timer class and numerous types to provide synchronized access to shared data. Table 6-3 lists some (but not all) of the core items.
Table 6-3. Select Types of the System.Treading Namespace
Examining the Thread ClassThe 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 defines a number of methods (both static and shared) that allow you to create new threads from a current thread, as well as suspend, stop, and destroy a given thread. First, consider the list of core static members given in Table 6-4.
Table 6-4. Static Members of the Thread Type
Thread also supports the object level members shown in Table 6-5. Table 6-5. Object Methods of the Thread Type
Spawning Secondary ThreadsWhen you wish to create additional threads to carry on some unit of work, you need to interact with the Thread class as well as a special threading-related delegate named ThreadStart. The general process is quite simple. To begin, you need to create a function to perform the background work. To keep things well focused, let's build a simple helper class that simply prints out a series of numbers by way of the DoSomeWork() member function: internal class WorkerClass { public void DoSomeWork() { // Get some information about this worker thread. Console.WriteLine("ID of worker thread is: {0}", Thread.CurrentThread.GetHashCode()); // Do the work. Console.Write("Worker says: "); for(int i = 0; i < 10; i++) { Console.Write(i + ", "); } Console.WriteLine(); } } Now assume you have another class (MainClass) that creates a new instance of WorkerClass. In order for the MainClass to continue processing its workflow, it creates and starts a new Thread that is used by the worker. In the code below, notice the | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||