Dot Net Processes AppDomains Contect Threads
Dot Net Processes AppDomains Contect Threads
All rights reserved. No part of this work may be reproduced or transmitted in any form or by any
means, electronic or mechanical, including photocopying, recording, or by any information
storage or retrieval system, without the prior written permission of the copyright owner and
the publisher.
ISBN : 1-59059-055-4
Printed and bound in the United States of America 12345678910
Trademarked names may appear in this book. Rather than use a trademark symbol with every
occurrence of a trademarked name, we use the names only in an editorial fashion and to the
benefit of the trademark owner, with no intention of infringement of the trademark.
Technical Reviewers: Gregory A. Beamer, Gary Cornell, Eric Gunnerson, Joe Nalewabau,
Kent Sharkey, Nick Symmonds, Pradeep Tapadiya
Editorial Directors: Dan Appleman, Gary Cornell, Simon Hayes, Martin Streicher,
Karen Watterson, John Zukowski
Assistant Publisher: Grace Wong
Copy Editors: Anne Friedman and Ami Knox
Proofreader: Liz Berry
Production Goddess: Susan Glinert Stevens
Indexer: Ron Strauss
Artist and Cover Designer: Kurt Krames
Manufacturing Manager: Tom Debolski
Distributed to the book trade in the United States by Springer-Verlag New York, Inc., 175 Fifth
Avenue, New York, NY, 10010 and outside the United States by Springer-Verlag GmbH & Co. KG,
Tiergartenstr. 17, 69112 Heidelberg, Germany.
In the United States: phone 1-800-SPRINGER, email [email protected], or visit
http://www.springer-ny.com. Outside the United States: fax +49 6221 345229, email
[email protected], or visit http://www.springer.de.
For information on translations, please contact Apress directly at 2560 Ninth Street, Suite 219,
Berkeley, CA 94710. Phone 510-549-5930, fax 510-549-5939, email [email protected], or visit
http://www.apress.com.
The information in this book is distributed on an “as is” basis, without warranty. Although every
precaution has been taken in the preparation of this work, neither the author(s) nor Apress shall
have any liability to any person or entity with respect to any loss or damage caused or alleged to
be caused directly or indirectly by the information contained in this work.
The source code for this book is available to readers at http://www.apress.com in the
Downloads section.
0554_Troelsen.book Page v Thursday, May 1, 2003 5:00 PM
Contents at a Glance
Introduction .......................................................................................................xxv
v
0554_Troelsen.book Page vi Thursday, May 1, 2003 5:00 PM
Contents at a Glance
vi
0554_Troelsen.book Page xxv Thursday, May 1, 2003 5:00 PM
Introduction
I REMEMBER A TIME years ago when I proposed a book to Apress regarding a forthcoming
software SDK named Next Generation Windows Services (NGWS). As you may already
know, NGWS eventually became what we now know as the .NET platform. My research
of the C# programming language and the .NET platform took place in parallel with the
authoring of the text. It was a fantastic project; however, I must confess that it was more
than a bit nerve-wracking writing about a technology that was undergoing drastic
changes over the course of its development. It pains me to recall how many chapters
had to be completely destroyed and rewritten during that time. Thankfully, after many
sleepless nights, the first edition of C# and the .NET Platform was published in con-
junction with the release of .NET Beta 2, circa the summer of 2001.
Since that point, I have been extremely happy and grateful to see that the first edition
of this text was very well received by the press and, most importantly, the readers. Over
the years, it was nominated as a Jolt award finalist (I lost . . . crap!) as well as the 2003
Referenceware programming book of the year (I won . . . cool!). Although the first edition
of this book has enjoyed a good run, it became clear that a second edition was in order—
not only to account for the changes brought about with the minor release of the .NET
platform, but to expand upon and improve the existing content. As I write this front-
matter, version 1.1 of the .NET platform is just about official, and I am happy to say that
C# and the .NET Platform, Second Edition is being released in tandem.
As in the first edition, this second edition presents the C# programming language and
.NET base class libraries using a friendly and approachable tone. I have never under-
stood the need some technical authors have to spit out prose that reads more like a
GRE vocabulary study guide than a readable discourse. As well, this new edition remains
focused on providing you with the information you need to build software solutions
today, rather than spending too much time focusing on esoteric details that few indi-
viduals will ever actually care about. To this end, when I do dive under the hood and
check out some more low-level functionality of the CLR (or blocks of CIL code), I promise
it will prove enlightening (rather than simple eye candy).
xxv
0554_Troelsen.book Page xxvi Thursday, May 1, 2003 5:00 PM
Introduction
Therefore, in this book, I have deliberately chosen to avoid creating examples that
tie the example code to a specific industry or vein of programming. Rather, I choose to
explain C#, OOP, the CLR, and the .NET base class libraries using industry-agnostic
examples. Rather than having every blessed example fill a grid with data, calculate
payroll, or whatnot, I’ll stick to subject matter we can all relate to: automobiles (with
some geometric structures and employees thrown in for good measure). And that’s
where you come in.
My job is to explain the C# programming language and the core aspects of the .NET
platform the best I possibly can. As well, I will do everything I can to equip you with the
tools and strategies you need to continue your studies at this book’s conclusion. Your
job is to take this information and apply it to your specific programming assignments.
I obviously understand that your projects most likely don’t revolve around automobiles
with pet names; however, that’s what applied knowledge is all about! Rest assured, once
you understand the concepts presented within this text, you will be in a perfect position
to build .NET solutions that map to your own unique programming environment.
xxvi
0554_Troelsen.book Page xxvii Thursday, May 1, 2003 5:00 PM
Introduction
xxvii
0554_Troelsen.book Page xxviii Thursday, May 1, 2003 5:00 PM
Introduction
contains numerous types that may be used out of the box, or serve as a foundation for
the development of strongly typed collections.
xxviii
0554_Troelsen.book Page xxix Thursday, May 1, 2003 5:00 PM
Introduction
using the types of the System.Threading namespace. Be aware that the information
presented here provides a solid foundation for understanding the .NET Remoting layer
(examined in Chapter 12).
xxix
0554_Troelsen.book Page xxx Thursday, May 1, 2003 5:00 PM
Introduction
xxx
0554_Troelsen.book Page xxxi Thursday, May 1, 2003 5:00 PM
Introduction
xxxi
0554_Troelsen.book Page xxxii Thursday, May 1, 2003 5:00 PM
Introduction
SOURCE CODE
is your cue that the example under discussion may be loaded into Visual Studio .NET
for further examination and modification. To do so, simply open the *.sln file found
in the correct subdirectory.
NOTE All of the source code for this book as been compiled using Visual
Studio .NET 2003. Sadly, *.sln files created with VS .NET 2003 cannot be
open using VS .NET 2002. If you are still currently running Visual Studio
.NET 2002, my advice is to simply create the appropriate project work-
space, delete the auto-generated C# files, and copy the supplied *.cs files
into the project using the Project | Add Existing Item menu selection.
Contacting Me
If you have any questions regarding this book’s source code, are in need of clarification
for a given example, or simply wish to offer your thoughts regarding the .NET platform,
feel free to drop me a line at the following e-mail address (to ensure your messages
don’t end up in my junk mail folder, please include “C# SE” in the title somewhere!):
[email protected].
Please understand that I will do my best to get back to you in a timely fashion;
however, like yourself, I get busy from time to time. If I don’t respond within a week or
two, do know I am not trying to be a jerk or don’t care to talk to you. I’m just busy (or if
I’m lucky, on vacation somewhere).
So then! Thanks for buying this text (or at least looking at it in the bookstore, trying
to decide if you will buy it). I hope you enjoy reading this book and put your newfound
knowledge to good use.
Take care,
Andrew Troelsen
Minneapolis, MN
xxxii
0554_Troelsen.book Page 451 Thursday, May 1, 2003 5:00 PM
CHAPTER 10
Processes, AppDomains,
Contexts, and Threads
IN THE PREVIOUS CHAPTER, you examined the steps taken by the CLR to resolve the
location of an externally referenced assembly. Here, you drill deeper into the consti-
tution of a .NET executable host and come to understand the relationship between
Win32 processes, application domains, contexts, and threads. In a nutshell, application
domains (or simply, AppDomains) are logical subdivisions within a given process,
which host a set of related .NET assemblies. As you will see, an application domain is
further subdivided into contextual boundaries, which are used to group together like-
minded .NET objects. Using the notion of context, the CLR is able to ensure that objects
with special needs are handled appropriately.
Once you have come to understand the relationship between processes, application
domains, and contexts, the remainder of this chapter examines how the .NET platform
allows you to manually spawn multiple threads of execution for use by your program
within its application domain. Using the types within the System.Threading namespace,
the task of creating additional threads of execution has become extremely simple (if
not downright trivial). Of course, the complexity of multithreaded development is not
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 (which you will
see is somewhat richer than raw Win32 threading primitives).
451
0554_Troelsen.book Page 452 Thursday, May 1, 2003 5:00 PM
Chapter 10
Task Manager utility (activated via the Ctrl+Shift+Esc keystroke combination) allows you to
view statistics regarding the set of processes running on a given machine, including its PID
and image name (Figure 10-1). (If you do not see a PID column, select the View | Select
Columns menu and check the PID box.)
Every Win32 process has at least one main “thread” that functions as the entry point
for the application. Formally speaking, the first thread created by a process’ entry point is
termed the primary thread. Simply put, a thread is a specific path of execution within a
Win32 process. Traditional Windows applications define the WinMain() method as the
application’s entry point. On the other hand, console application provides the main()
method for the same purpose.
Processes that contain a single primary thread of execution are intrinsically “thread-
safe,” given the fact that there is only one thread that can access the data in the appli-
cation at a given time. However, a single-threaded process (especially one that is GUI-
based) will often appear a bit unresponsive to the user if this single thread is performing
a complex operation (such as printing out a lengthy text file, performing an exotic
calculation, or attempting to connect to a remote server thousands of miles away).
Given this potential drawback of single-threaded applications, the Win32 API makes
it is possible for the primary thread to spawn additional secondary threads (also termed
worker 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 shared points of data.
As you may have guessed, developers typically create additional threads to help
improve the program’s overall responsiveness. Multithreaded processes provide the
illusion that numerous activities are happening at more or less the same time.
452
0554_Troelsen.book Page 453 Thursday, May 1, 2003 5:00 PM
Shared Data
Thread A Thread B
453
0554_Troelsen.book Page 454 Thursday, May 1, 2003 5:00 PM
Chapter 10
454
0554_Troelsen.book Page 455 Thursday, May 1, 2003 5:00 PM
ExitTime This property gets the time stamp associated with the
process that has terminated (represented with a
DateTime type).
455
0554_Troelsen.book Page 456 Thursday, May 1, 2003 5:00 PM
Chapter 10
Threads This property gets the set of threads that are running
in the associated process (represented via an array of
ProcessThread types).
456
0554_Troelsen.book Page 457 Thursday, May 1, 2003 5:00 PM
Once you have obtained the array of Process types, you are able to trigger any of the
members seen in Table 10-2. Here, simply dump the process identifier (PID) and the
name of each process. Assuming the Main() method has been updated to call this
helper function, you will see something like the output in Figure 10-3.
457
0554_Troelsen.book Page 458 Thursday, May 1, 2003 5:00 PM
Chapter 10
As you can see, the Threads property of the System.Diagnostics.Process type provides
access to the ProcessThreadCollection class. Here, we are printing out the assigned
thread ID, start time, and priority level of each thread in the process specified by the
client. Thus, if you update your program’s Main() method to prompt the user for a PID
to investigate:
you would find output along the lines of the Figure 10-4.
458
0554_Troelsen.book Page 459 Thursday, May 1, 2003 5:00 PM
The ProcessThread type has additional members of interest beyond Id, StartTime,
and PriorityLevel. Table 10-3 documents some members of interest.
459
0554_Troelsen.book Page 460 Thursday, May 1, 2003 5:00 PM
Chapter 10
Now before reading any further, be very aware that the ProcessThread type is not
the entity used to create, suspend, or kill threads under the .NET platform. Rather,
ProcessThread is a vehicle used to obtain diagnostic information for the active threads
within a running process.
To illustrate one possible invocation of this function, let’s check out the loaded modules
for the process hosting your current console application (ProcessManipulator). To do
so, run the application, identify the PID assigned to ProcessManipulator.exe, and pass
this value to the EnumModsForPid() method (be sure to update your Main() method
accordingly). Once you do, you may be surprised to see the list of *.dlls used for a
simple console application (atl.dll, mfc42u.dll, oleaut32.dll and so forth.) Figure 10-5
shows a test run.
460
0554_Troelsen.book Page 461 Thursday, May 1, 2003 5:00 PM
The static Process.Start() method has been overloaded a few times, however. At
minimum you will need to specify the friendly name of the process you wish to launch
(such as MS Internet Explorer). This example makes use of a variation of the Start()
method that allows you to specify any additional arguments to pass into the program’s
entry point (i.e., the Main() method).
The Start() method also allows you to pass in a System.Diagnostics.ProcessStartInfo
type to specify additional bits of information regarding how a given process should
461
0554_Troelsen.book Page 462 Thursday, May 1, 2003 5:00 PM
Chapter 10
come into life. Here is the formal definition of ProcessStartInfo (see online Help for full
details of this type):
Regardless of which version of the Process.Start() method you invoke, do note that
you are returned a reference to the newly activated process. When you wish to terminate
the process, simply call the instance level Kill() method.
462
0554_Troelsen.book Page 463 Thursday, May 1, 2003 5:00 PM
• AppDomains are a key aspect of the OS-neutral nature of the .NET platform,
given that this logical division abstracts away the differences in how an under-
lying operating system represents a loaded executable.
• AppDomains are far less expensive in terms of processing power and memory
than a full blown process (for example, the CLR is able to load and unload appli-
cation domains much quicker than a formal process).
As suggested in the previous hit-list, 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). Given this factoid, be very aware that appli-
cations that run in unique AppDomains are unable to share any information of any
kind (global variables or static fields) unless they make use of the .NET Remoting
protocol (examined in Chapter 12) to marshal the data.
Understand that while a single process may host multiple AppDomains, this is not
always the case. At the very least an OS process will host what is termed the default
application domain. This specific application domain is automatically created by the
CLR at the time the process launches. After this point, the CLR creates additional appli-
cation domains on an as-needed basis. If the need should arise (which it most likely
will not for a majority of your .NET endeavors), you are also able to programmatically
create application domains at runtime within a given process using static methods of
the System.AppDomain class. This class is also useful for low-level control of appli-
cation domains. Key members of this class are shown in Table 10-4.
463
0554_Troelsen.book Page 464 Thursday, May 1, 2003 5:00 PM
Chapter 10
BaseDirectory This property returns the base directory that the assembly
resolver used to probe for dependent assemblies.
GetAssemblies() Gets the set of .NET assemblies that have been loaded
into this application domain.
Unlike the Process type, the GetAssemblies() method
will only return the list of true-blue .NET binaries.
COM-based or C-based binaries are ignored.
In addition, the AppDomain type also defines a small set of events that correspond
to various aspects of an application domain’s life-cycle (Table 10-5).
Now assume you have updated the Main() method to obtain a reference to the
current application domain before invoking PrintAllAssembliesInAppDomain(), using
the AppDomain.CurrentDomain property. To make things a bit more interesting,
notice that the Main() method launches a message box to force the assembly resolver
to load the System.Windows.Forms.dll and System.dll assemblies (so be sure to set a
reference to these assemblies and update your “using” statements appropriately):
465
0554_Troelsen.book Page 466 Thursday, May 1, 2003 5:00 PM
Chapter 10
Now, if you run the application again (Figure 10-7), notice that the
System.Windows.Forms.dll and System.dll assemblies are only loaded within the
default application domain! This may seem counterintuitive if you have a background
in traditional Win32 (as you might suspect that both application domains have access
to the same assembly set). Recall, however, that an assembly loads into an application
domain, not directly into the process itself.
466
0554_Troelsen.book Page 467 Thursday, May 1, 2003 5:00 PM
The AppDomainManipulator.exe
mscorlib.dll mscorlib.dll
system.dll
System.Windows.Forms.dll
MyAppDomain.exe
Chapter 10
If you wish to be notified when the default AppDomain is unloaded, modify your
application to support the following event logic:
468
0554_Troelsen.book Page 469 Thursday, May 1, 2003 5:00 PM
out of a given context. This layer of interception allows CLR to adjust the current method
invocation to conform to the contextual settings of a given type.
Just as a process defines a default AppDomain, every application domain has a
default context. This default context (sometimes referred to as context 0, given that it is
always the first context created within an application domain) is used to group together
.NET objects that have no specific or unique contextual needs. As you may expect, a
vast majority of your .NET class types will be loaded into context 0. If the CLR determines
a newly created object has special needs, a new context boundary is created within the
hosting application domain. Figure 10-9 illustrates the process, AppDomain, context
relationship.
Context 1 Context 1
Context 2 Context 2
On the other hand, objects that do demand contextual allocation are termed
context-bound objects, and must derive from the System.ContextBoundObject base
class. This base class solidifies the fact that the object in question can only function
appropriately within the context in which it was created.
In addition to deriving from System.ContextBoundObject, a context-
sensitive type will also be adorned with a special category of .NET attributes
termed (not surprisingly) context attributes. All context attributes derive from
the System.Runtime.Remoting.Contexts.ContextAttribute base class, which is
469
0554_Troelsen.book Page 470 Thursday, May 1, 2003 5:00 PM
Chapter 10
defined as follows (note this class type implements two context-centric interfaces,
IContextAttribute and IContextProperty):
The .NET base class libraries define numerous context attributes that describe
specific runtime requirements (such as thread synchronization and URL activation).
Given the role of .NET context, it should stand to reason that if a context-bound object
were to somehow end up in an incompatible context, bad things are guaranteed to
occur at the most inopportune times.
using System.Runtime.Remoting.Contexts;
...
// This context-bound type will only be loaded into a
// synchronized (and hence, thread safe) context.
[Synchronization]
public class MyThreadSafeObject : ContextBoundObject
{}
470
0554_Troelsen.book Page 471 Thursday, May 1, 2003 5:00 PM
As you will see in greater detail later in this chapter, classes that are attributed with
the [Synchronization] attribute are loaded into a thread-safe context. Given the special
contextual needs of the MyThreadSafeObject class type, imagine the problems that
would occur if an allocated object were moved from a synchronized context into a non-
synchronized context. The object is suddenly no longer thread-safe and thus becomes
a candidate for massive data corruption, as numerous threads are attempting to interact
with the (now thread-volatile) reference object. This is obviously a huge problem, given
that the code base has not specifically wrapped thread-sensitive resources with hard-
coded synchronization logic.
471
0554_Troelsen.book Page 472 Thursday, May 1, 2003 5:00 PM
Chapter 10
472
0554_Troelsen.book Page 473 Thursday, May 1, 2003 5:00 PM
473
0554_Troelsen.book Page 474 Thursday, May 1, 2003 5:00 PM
Chapter 10
• A given AppDomain consists of one to many contexts. Using a context, the CLR
is able to group special needs objects into a logical container, to ensure that their
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. However, this background discussion
has provided a solid foundation regarding how the CLR creates, processes, and destroys
specific threads of execution (as well as increased your understanding of some
underlying CLR concepts).
A single thread may also be moved into a particular context at any given time, and may
be relocated within a new context at the whim of the CLR. If you wish to programmatically
474
0554_Troelsen.book Page 475 Thursday, May 1, 2003 5:00 PM
discover the current context a thread happens to be executing in, make use of the static
Thread.CurrentContext property:
As you would guess, 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 are able to
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 underlying model.
475
0554_Troelsen.book Page 476 Thursday, May 1, 2003 5:00 PM
Chapter 10
476
0554_Troelsen.book Page 477 Thursday, May 1, 2003 5:00 PM
ThreadStart Delegate that specifies the method to call for a given Thread.
ThreadState This enum specifies the valid states a thread may take
(Running, Aborted, etc.).
Thread also supports the object level members shown in Table 10-8.
477
0554_Troelsen.book Page 478 Thursday, May 1, 2003 5:00 PM
Chapter 10
using System.Threading;
...
static void Main(string[] args)
{
// Get some info about the current thread.
Thread primaryThread = Thread.CurrentThread;
// Get name of current AppDomain and context ID.
478
0554_Troelsen.book Page 479 Thursday, May 1, 2003 5:00 PM
Naming Threads
When you run this application, notice how the name of the default thread is currently
an empty string. Under .NET, it is possible to assign a human-readable string to a
thread using the Name property. Thus, if you wish to be able to programmatically
identify the primary thread via the moniker “ThePrimaryThread,” you could write
the following:
479
0554_Troelsen.book Page 480 Thursday, May 1, 2003 5:00 PM
Chapter 10
If you wish to specify support for the MTA, simply adjust the attribute:
[MTAThread]
static void Main(string[] args)
{
// COM objects will be placed into the MTA.
}
Of course, if you don’t know (or care) about classic COM objects, you can simply
leave the [STAThread] attribute on your Main() method. Doing so will keep any COM
types thread-safe without further work on your part. If you don’t make use of COM types
within the Main() method, the [STAThread] attribute does nothing.
Always keep in mind that a thread with the value of ThreadPriority.Highest is not
necessarily guaranteed to given the highest precedence. Again, if the thread scheduler
is preoccupied with a given task (e.g., synchronizing an object, switching threads,
moving threads, or whatnot) the priority level will most likely be altered accordingly.
However, all things being equal, the CLR will read these values and instruct the thread
scheduler how to best allocate time slices. All things still being equal, threads with an
identical thread priority should each receive the same amount of time to perform
their work.
NOTE Again, you will seldom (if ever) need to directly alter a thread's
priority level. In theory, it is possible to jack up the priority level on a set
of threads, thereby preventing lower priority threads from executing at
their required levels (so use caution).
480
0554_Troelsen.book Page 481 Thursday, May 1, 2003 5:00 PM
NOTE The target for the ThreadStart delegate cannot take any
arguments and must return void.
Now, within Main(), create a new Thread class and specify a new ThreadStart delegate
as a constructor parameter (note the lack of parentheses in the constructor when you
give the method name). To inform the CLR that this new thread is ready to run, call the
Start() method (but always remember that Start() doesn’t actually start the thread).
Starting a thread is a nondeterministic operation under the total control of the CLR—
you can’t do anything to force the CLR to execute your thread. It will do so on its own
time and on its own terms:
[STAThread]
static void Main(string[] args)
{
...
// Start a secondary thread.
Thread secondaryThread = new Thread(new ThreadStart(MyThreadProc));
secondaryThread.Start();
}
481
0554_Troelsen.book Page 482 Thursday, May 1, 2003 5:00 PM
Chapter 10
One question that may be on your mind is exactly when a thread terminates. By
default, a thread terminates as soon as the function used to create it in the ThreadStart
delegate has exited.
• Foreground threads: Foreground threads have the ability to prevent the current
application from terminating. The CLR will not shut down an application (which
is to say, unload the hosting AppDomain) until all foreground threads have ended.
It is important to note that foreground and background threads are not synonymous
with primary and worker threads. By default, every thread you create via the Thread.Start()
method is automatically a foreground thread. Again, this means that the AppDomain
will not unload until all threads of execution have completed their units of work. In
most cases, this is exactly the behavior you require.
For the sake of argument, however, assume that you wish to spawn a secondary
thread that should behave as a background thread. Again, this means that the method
pointed to by the Thread type (via the ThreadStart delegate) should be able to halt
safely as soon as all foreground threads are done with their work. Configuring such a
thread is as simple as setting the IsBackground property to true:
482
0554_Troelsen.book Page 483 Thursday, May 1, 2003 5:00 PM
Now, to illustrate the distinction, assume that the MyThreadProc() method has been
updated to print out 1000 lines to the console, pausing for 5 milliseconds between iter-
ations using the Thread.Sleep() method (more on the Thread.Sleep() method later in
this chapter):
If you run the application again, you will find that the for loop is only able to print
out a tiny fraction of the values, given that the secondary Thread object has been
configured as a background thread. Given that the Main() method has spawned a
primary foreground thread, as soon as the secondary thread has been started, it is
ready for termination.
Now, you are most likely to simply allow all threads used by a given application to
remain configured as foreground threads. If this is the case, all threads must finish their
work before the AppDomain is unloaded from the hosting process. Nevertheless, marking
a thread as a background type can be helpful when the worker-thread in question is
performing noncritical tasks or helper tasks that are no longer needed when the main
task of the program is over.
483
0554_Troelsen.book Page 484 Thursday, May 1, 2003 5:00 PM
Chapter 10
Now assume the Main() method creates a new instance of WorkerClass. For the
primary thread to continue processing its workflow, create and start a new Thread
that is configured to execute the DoSomeWork() method of the WorkerClass type:
484
0554_Troelsen.book Page 485 Thursday, May 1, 2003 5:00 PM
If you run the application you would find each thread has a unique hash code
(which is a good thing, as you should have two separate threads at this point).
Next, update the MainClass such that it launches a Windows Forms message
box directly after it creates the worker thread (don’t forget to set a reference to
System.Windows.Forms.dll):
If you were to now run the application, you would see that the message box is
displayed and can be moved around the desktop while the worker thread is busy
pumping numbers to the console (Figure 10-13).
485
0554_Troelsen.book Page 486 Thursday, May 1, 2003 5:00 PM
Chapter 10
Now, contrast this behavior with what you might find if you had a single-threaded
application. Assume the Main() method has been updated with logic that allows the
user to enter the number of threads used within the current AppDomain (one or two):
As you can guess, if the user enters the value “1” he or she must wait for all 30,000
numbers to be printed before seeing the message box appear, given that there is only a
single thread in the AppDomain. However, if the user enters “2” he or she is able to
interact with the message box while the secondary thread spins right along.
486
0554_Troelsen.book Page 487 Thursday, May 1, 2003 5:00 PM
this to pause a program. To illustrate, let’s update the WorkerClass again. This time
around, the DoSomeWork() method does not print out 30,000 lines to the console, but
10 lines. The trick is, between each call to Console.WriteLine(), this worker thread is put
to sleep for approximately 2 seconds.
Now run your application a few times and specify both threading options. You will
find radically different behaviors based on your choice of thread number.
Concurrency Revisited
Given this previous example, you might be thinking that threads are the magic bullet
you have been looking for. Simply create threads for each part of your application and
the result will be increased application performance to the user. You already know this
is a loaded question, as the previous statement is not necessarily true. If not used care-
fully and thoughtfully, multithreaded programs are slower than single threaded programs.
Even more important is the fact that each and every thread in a given AppDomain
has direct access to the shared data of the application. In the current example, this is
not a problem. However, imagine what might happen if the primary and secondary
threads were both modifying a shared point of data. As you know, the thread scheduler
will force threads to suspend their work at random. Since this is the case, what if thread
A is kicked out of the way before it has fully completed its work? Again, thread B is now
reading unstable data.
To illustrate, let’s build another C# console application named MultiThreadSharedData.
This application also has a class named WorkerClass, which maintains a private
487
0554_Troelsen.book Page 488 Thursday, May 1, 2003 5:00 PM
Chapter 10
The Main() method is responsible for creating three uniquely named secondary
threads of execution, each of which is making calls to the same instance of the
WorkerClass type:
488
0554_Troelsen.book Page 489 Thursday, May 1, 2003 5:00 PM
Now before you see some test runs, let’s recap the problem. The primary thread within
this AppDomain begins life by spawning three secondary worker threads. Each worker
thread is told to make calls on the DoSomeWork() method of a single WorkerClass
instance. Given that we have taken no precautions to lock down the object’s shared
resources, there is a good chance that a given thread will be kicked out of the way
before the WorkerClass is able to print out the results for the previous thread. Because
we don’t know exactly when (or if) this might happen, we are bound to get unpredictable
results. For example, you might find the output shown in Figure 10-14.
Now run the application a few more times. Figure 10-15 shows another possibility
(note the ordering among thread names).
Humm. There are clearly some problems here. As each thread is telling the WorkerClass
to “do some work,” the thread scheduler is happily swapping threads in the background.
The result is inconsistent output. What we need is a way to programmatically enforce
synchronized access to the shared resources.
489
0554_Troelsen.book Page 490 Thursday, May 1, 2003 5:00 PM
Chapter 10
Now, once a thread enters into a locked block of code, the token (in this case, a ref-
erence to the current object) is inaccessible by other threads until the lock is released.
Thus, if threadA has obtained the lock token, and threadB or threadC are attempting to
enter, they must wait until threadA relinquishes the lock.
NOTE If you are attempting to lock down code in a static method, you
obviously cannot use the “this” keyword. If this is the case, you can
simply pass in the System.Type of the current class using the C# “typeof”
operator (although any object reference will work).
If you now rerun the application, you can see that the threads are instructed to
politely wait in line for the current thread to finish its business (Figure 10-16).
490
0554_Troelsen.book Page 491 Thursday, May 1, 2003 5:00 PM
491
0554_Troelsen.book Page 492 Thursday, May 1, 2003 5:00 PM
Chapter 10
finally
{
// Error or not, you must exit the monitor
// and release the token.
Monitor.Exit(this);
}
}
}
If you run the modified application, you will see no changes in the output (which is
good). Here, you make use of the static Enter() and Exit() members of the Monitor type,
to enter (and leave) a locked block of code. Now, given that the “lock” keyword seems to
require less code than making explicit use of the System.Threading.Monitor type, you
may wonder about the benefits. The short answer is control.
If you make use of the Monitor type, you are able to instruct the active thread to wait
for some duration of time (via the Wait() method), inform waiting threads when the
current thread is completed (via the Pulse() and PulseAll() methods), and so on. As you
would expect, in a great number of cases, the C# “lock” keyword will fit the bill. If you
are interested in checking out additional members of the Monitor class, consult
online Help.
Although it might not seem like it from the onset, the process of atomically altering
a single value is quite common in a multithreaded environment. Thus, rather than
writing synchronization code such as the following:
492
0554_Troelsen.book Page 493 Thursday, May 1, 2003 5:00 PM
int i = 9;
lock(this)
{ i++; }
Likewise, if you wish to assign the value of a previously assigned System.Int32 to the
value 83, you can avoid the need to an explicit lock statement (or Monitor logic) and
make use of the Interlocked.Exchange() method:
int i = 9;
Interlocked.Exchange(ref i, 83);
Finally, if you wish to test two values for equality to change the point of comparison in a
thread-safe manner, you would be able to leverage the Interlocked.CompareExchange()
method as follows:
using System.Runtime.Remoting.Contexts;
...
// This context-bound type will only be loaded into a
// synchronized (and hence, thread-safe) context.
[Synchronization]
public class MyThreadSafeObject : ContextBoundObject
{ /* all methods on class are now thread safe */}
In some ways, this approach can be seen as the lazy approach to writing thread-safe
code, given that we are not required to dive into the details about which aspects of the
type are truly manipulating thread-sensitive data. The major downfall of this approach,
however, is that even if a given method is not making use of thread-sensitive data, the
CLR will still lock invocations to the method. Obviously, this could degrade the overall
functionality of the type, so use this technique with care.
493
0554_Troelsen.book Page 494 Thursday, May 1, 2003 5:00 PM
Chapter 10
Figure 10-17. Many (but not all) .NET types are already thread-safe .
Sadly, many .NET types in the base class libraries are not thread-safe, and therefore,
you will have to make use of the various locking techniques you have examined to
ensure the object is able to survive multiple requests from the thread base.
494
0554_Troelsen.book Page 495 Thursday, May 1, 2003 5:00 PM
class TimePrinter
{
static void PrintTime(object state)
{
Console.WriteLine("Time is: {0}",
DateTime.Now.ToLongTimeString());
}
...
}
Notice how this method has a single parameter of type System.Object and returns
void. This is not optional, given that the TimerCallback delegate can only call methods
that match this signature. The value passed into the target of your TimerCallback del-
egate can be any bit of information whatsoever (in the case of the e-mail example, this
parameter might represent the name of the MS Exchange server to interact with during
the process). Also note that given that this parameter is indeed a System.Object, you
are able to pass in multiple arguments using a System.Array type.
The next step would be to configure an instance of the TimerCallback type and pass
it into the Timer object. In addition to a TimerCallback delegate, the Timer constructor
also allows you to specify the optional parameter information to pass into the delegate
target, the interval to poll the method, as well as the amount of time to wait before
making the first call. For example:
In this case, the PrintTime() method will be called roughly every second, and will
pass in no additional information to said method. If you did wish to send in some infor-
mation for use by the delegate target, simply substitute the null value of the second
constructor parameter with the appropriate information. For example, ponder the
following updates:
495
0554_Troelsen.book Page 496 Thursday, May 1, 2003 5:00 PM
Chapter 10
Summary
The point of this chapter was to expose the internal composition of a .NET executable
image. 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
on multiple application domains, which represent isolated and independent boundaries
within a process. As you recall, 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.
496
0554_Troelsen.book Page 497 Thursday, May 1, 2003 5:00 PM
The remainder of this chapter examined the role of the System.Threading namespace.
As you have seen, when an application creates additional threads of execution, the
result is that the program in question is able to carry out numerous tasks at (what
appears to be) the same time. Finally, the chapter examined various manners in which
you can mark thread-sensitive blocks of code to ensure that shared resources do not
become unusable units of bogus data.
497