Threads
are responsible for multitasking within a single application. The
System.Threading
namespace provides a wealth of
classes and interfaces to manage multithreaded programming. The
majority of programmers might never need to manage threads
explicitly, however, because the CLR abstracts much of the threading
support into classes that simplify most threading tasks. For example,
in Chapter 21 you will see how to create
multithreaded reading and writing streams without resorting to
managing the threads yourself.
The first part of this chapter shows you how to create, manage, and kill threads. Even if you don’t create your own threads explicitly, you’ll want to ensure that your code can handle multiple threads if it’s run in a multithreading environment. This concern is especially important if you are creating components that might be used by other programmers in a program that supports multithreading. It is particularly significant to remoting and web services developers. Although web services (covered in Chapter 15) have many attributes of desktop applications, they are run on the server, generally lack a user interface, and force the developer to think about server-side issues such as efficiency and multithreading.
The second part of this chapter focuses on synchronization. When you have a limited resource (such as a database connection), you may need to restrict access to that resource to one thread at a time. A classic analogy is to a restroom on an airplane. You want to allow access to the restroom for only one person at a time. This is done by putting a lock on the door. When passengers want to use the restroom, they try the door handle; if it is locked, they either go away and do something else, or wait patiently in line with others who want access to the resource. When the resource becomes free, one person is taken off the line and given the resource, which is then locked again.
At times, various threads might want to access a resource in your program, such as a file. It might be important to ensure that only one thread has access to your resource at a time, and so you will lock the resource, allow a thread access, and then unlock the resource. Programming locks can be fairly sophisticated, ensuring a fair distribution of resources.
Threads are typically created when you want a program to do two things at once. For example, assume you are calculating pi (3.141592653589...) to the 10 billionth place. The processor will happily begin computing this, but nothing will write to the user interface while it is working. Because computing pi to the 10 billionth place will take a few million years, you might like the processor to provide an update as it goes. In addition, you might want to provide a Stop button so that the user can cancel the operation at any time. To allow the program to handle the click on the Stop button, you will need a second thread of execution.
Another common place to use threading is when you must wait for an event, such as user input, a read from a file, or receipt of data over the network. Freeing the processor to turn its attention to another task while you wait (such as computing another 10,000 values of pi) is a good idea, and it makes your program appear to run more quickly.
On the flip side, note that in some circumstances, threading can actually slow you down. Assume that in addition to calculating pi, you also want to calculate the Fibonacci series (1,1,2,3,5,8,13,21...). If you have a multiprocessor machine, this will run faster if each computation is in its own thread. If you have a single-processor machine (as most users do), computing these values in multiple threads will certainly run slower than computing one and then the other in a single thread because the processor must switch back and forth between the two threads. This incurs some overhead.
The simplest
way to create a thread is to create a new instance of the
Thread
class. The
Thread
constructor takes a single argument: a
delegate
instance. The CLR provides the
ThreadStart
delegate class specifically for this
purpose, which points to a method you designate. This allows you to
construct a thread and to say to it, “When you
start, run this method.” The
ThreadStart
delegate declaration is:
public delegate void ThreadStart();
As you can see, the method you attach to this delegate must take no
parameters and must return void
. Thus, you might
create a new thread like this:
Thread myThread = new Thread( new ThreadStart(myFunc) );
For example, you might create two worker threads, one that counts up from zero:
public void Incrementer() { for (int i =0;i<1000;i++) { Console.WriteLine("Incrementer: {0}", i); } }
and one that counts down from 1,000:
public void Decrementer() { for (int i = 1000;i>=0;i--) { Console.WriteLine("Decrementer: {0}", i); } }
To run these in threads, create two new threads, each initialized
with a ThreadStart
delegate. These in turn would
be initialized to the respective member functions:
Thread t1 = new Thread( new ThreadStart(Incrementer) ); Thread t2 = new Thread( new ThreadStart(Decrementer) );
Instantiating these threads doesn’t start them
running. To do so you must call the
Start
method on the Thread
object itself:
t1.Start(); t2.Start();
If you don’t take further action, the thread stops when the function returns. You’ll see how to stop a thread before the function ends later in this chapter.
Example 20-1 is the full program and its output. You
will need to add a using
statement for
System.Threading
to make the compiler aware of the
Thread
class. Notice the output, where you can see
the processor switching from t1
to
t2
.
Example 20-1. Using threads
#region Using directives using System; using System.Collections.Generic; using System.Text; using System.Threading; #endregion namespace UsingThreads { classTester { static void Main( ) { // make an instance of this class Tester t = new Tester( ); Console.WriteLine( "Hello" ); // run outside static Main t.DoTest( ); } public void DoTest( ) { // create a thread for the Incrementer // pass in a ThreadStart delegate // with the address of Incrementer Thread t1 = new Thread( new ThreadStart( Incrementer ) ); // create a thread for the Decrementer // pass in a ThreadStart delegate // with the address of Decrementer Thread t2 = new Thread( new ThreadStart( Decrementer ) ); // start the threads t1.Start( ); t2.Start( ); } // demo function, counts up to 1K public void Incrementer( ) { for ( int i = 0; i < 1000; i++ ) { System.Console.WriteLine( "Incrementer: {0}", i ); } } // demo function, counts down from 1k public void Decrementer( ) { for ( int i = 1000; i >= 0; i-- ) { System.Console.WriteLine( "Decrementer: {0}", i ); } } } } Output (excerpt): Incrementer: 102 Incrementer: 103 Incrementer: 104 Incrementer: 105 Incrementer: 106 Decrementer: 1000 Decrementer: 999 Decrementer: 998 Decrementer: 997
The processor allows the first thread to run long enough to count up to 106. Then, the second thread kicks in, counting down from 1,000 for a while. Then the first thread is allowed to run. When I run this with larger numbers, I notice that each thread is allowed to run for about 100 numbers before switching.
When you tell a thread to stop processing and wait until a second thread completes its work, you are said to be joining the first thread to the second. It is as if you tied the tip of the first thread on to the tail of the second—hence “joining” them.
To join thread 1 (t1
) onto thread 2
(t2
), write:
t2.Join( );
If this statement is executed in a method in thread
t1
, t1
will halt and wait until
t2
completes and exits. For example, you might ask
the thread in which Main( )
executes to wait for
all our other threads to end before it writes its concluding message.
In this next code snippet, assume you’ve created a
collection of threads named myThreads
. Iterate
over the collection, joining the current thread to each thread in the
collection in turn:
foreach (Thread myThread in myThreads) { myThread.Join(); } Console.WriteLine("All my threads are done.");
The final message All
my
threads
are
done
isn’t be printed until all
the threads have ended. In a production environment, you might start
up a series of threads to accomplish some task (e.g., printing,
updating the display, etc.) and not want to continue the main thread
of execution until the worker threads are completed.
At times, you want to suspend your thread for a short while. You might, for example, like your clock thread to suspend for about a second in between testing the system time. This lets you display the new time about once a second without devoting hundreds of millions of machine cycles to the effort.
The Thread
class offers a public static method,
Sleep
, for just this purpose. The method is
overloaded; one version takes an int
, the other a
timeSpan
object. Each represents the number of
milliseconds you want the thread suspended for, expressed either as
an int
(e.g., 2,000 = 2,000 milliseconds or 2
seconds) or as a timeSpan
.
Although timeSpan
objects can measure
ticks (100 nanoseconds), the
Sleep()
method’s granularity is
in milliseconds (1,000,000 nanoseconds).
To cause your thread to sleep for one second, you can invoke the
static method of Thread
.Sleep
,
which suspends the thread in which it is invoked:
Thread.Sleep(1000);
At times, you’ll pass zero for the amount of time to sleep; this signals the thread scheduler that you’d like your thread to yield to another thread, even if the thread scheduler might otherwise give your thread a bit more time.
If you modify Example 20-1 to add a
Thread.Sleep(1)
statement after each
WriteLine()
, the output changes significantly:
for (int i =0;i<1000;i++) { Console.WriteLine( "Incrementer: {0}", i); Thread.Sleep(1); }
This small change is sufficient to give each thread an opportunity to run once the other thread prints one value. The output reflects this change:
Incrementer: 0 Incrementer: 1 Decrementer: 1000 Incrementer: 2 Decrementer: 999 Incrementer: 3 Decrementer: 998 Incrementer: 4 Decrementer: 997 Incrementer: 5 Decrementer: 996 Incrementer: 6 Decrementer: 995
Typically, threads
die after running their course. You can, however, ask a thread to
kill itself. The cleanest way is to set a
KeepAlive
Boolean flag that the thread can check
periodically. When the flag changes state (e.g., goes from true to
false) the thread can stop itself.
An alternative is to call
Thread.Interrupt
which asks the thread to kill itself.
Finally, in desperation, and if you are shutting down your
application in any case, you may call
Thread.Abort
. This causes a
ThreadAbortException
exception to be thrown, which the
thread can catch.
The thread ought to treat the ThreadAbortException
exception as a signal that it is time to exit immediately. In any
case, you don’t so much kill a thread as politely
request that it commit suicide.
You might wish to kill a thread in reaction to an event, such as the
user clicking the Cancel button. The event handler for the Cancel
button might be in thread t1
, and the event it is
canceling might be in thread t2
. In your event
handler, you can call
Abort
on
t1
:
t1.Abort();
An exception will be raised in
t1
’s currently running method
that t1
can catch.
In Example 20-2, three threads are created and stored
in an array of Thread
objects. Before the
Threads
are started, the
IsBackground
property is set to
true
(background threads are exactly like
foreground threads, except that they don’t stop a
process from terminating). Each thread is then started and named
(e.g., Thread1
, Thread2
, etc.).
A message is displayed indicating that the thread is started, and
then the main thread sleeps for 50 milliseconds before starting up
the next thread.
After all three threads are started and another 50 milliseconds have
passed, the first thread is aborted by calling
Abort()
. The main thread then joins all three of
the running threads. The effect of this is that the main thread will
not resume until all the other threads have completed. When they do
complete, the main thread prints a message: All
my
threads
are
done
. The complete source
is displayed in Example 20-2.
Example 20-2. Interrupting a thread
#region Using directives using System; using System.Collections.Generic; using System.Text; using System.Threading; #endregion namespace InterruptingThreads { classTester { static void Main( ) { // make an instance of this class Tester t = new Tester( ); // run outside static Main t.DoTest( ); } public void DoTest( ) { // create an array of unnamed threads Thread[] myThreads = { new Thread( new ThreadStart(Decrementer) ), new Thread( new ThreadStart(Incrementer) ), new Thread( new ThreadStart(Decrementer) ), new Thread( new ThreadStart(Incrementer) ) }; // start each thread int ctr = 1; foreach (Thread myThread in myThreads) { myThread.IsBackground = true; myThread.Start( ); myThread.Name = "Thread" + ctr.ToString( ); ctr++; Console.WriteLine("Started thread {0}", myThread.Name); Thread.Sleep(50); } // ask the first thread to stop myThreads[0].Interrupt( ); // tell the second thread to abort immediately myThreads[1].Abort( ); // wait for all threads to end before continuing foreach (Thread myThread in myThreads) { myThread.Join( ); } // after all threads end, print a message Console.WriteLine("All my threads are done."); } // demo function, counts down from 100 public void Decrementer( ) { try { for (int i = 100; i >= 0; i--) { Console.WriteLine( "Thread {0}. Decrementer: {1}", Thread.CurrentThread.Name, i); Thread.Sleep(1); } } catch (ThreadAbortException) { Console.WriteLine( "Thread {0} aborted! Cleaning up...", Thread.CurrentThread.Name); } catch (System.Exception e) { Console. WriteLine("Thread has been interrupted "); } finally { Console.WriteLine( "Thread {0} Exiting. ", Thread.CurrentThread.Name); } } // demo function, counts up to 100 public void Incrementer( ) { try { for (int i = 0; i < 100; i++) { Console.WriteLine( "Thread {0}. Incrementer: {1}", Thread.CurrentThread.Name, i); Thread.Sleep(1); } } catch (ThreadAbortException) { Console.WriteLine( "Thread {0} aborted!", Thread.CurrentThread.Name); } catch (System.Exception e) { Console. WriteLine("Thread has been interrupted"); } finally { Console.WriteLine( "Thread {0} Exiting. ", Thread.CurrentThread.Name); } } } } Output (excerpt): Started thread Thread1 Thread Thread1. Decrementer: 100 Thread Thread1. Decrementer: 99 Started thread Thread2 Thread Thread2. Incrementer: 0 Thread Thread1. Decrementer: 98 Started thread Thread3 Thread Thread3. Decrementer: 100 Thread Thread1. Decrementer: 97 Thread Thread2. Incrementer: 1 Started thread Thread4 Thread Thread4. Incrementer: 0 Thread Thread2 aborted! Thread Thread3. Decrementer: 99 Thread Thread2 Exiting. Thread has been interrupted Thread Thread3. Decrementer: 98 Thread Thread4. Incrementer: 1 Thread Thread1 Exiting. Thread Thread3. Decrementer: 97 Thread Thread3. Decrementer: 1 Thread Thread4. Incrementer: 98 Thread Thread3. Decrementer: 0 Thread Thread4. Incrementer: 99 Thread Thread3 Exiting. Thread Thread4 Exiting. All my threads are done.
You see the first thread start and decrement from 100 to 99. The
second thread starts, and the two threads are interleaved for a while
until the third and fourth threads start. After a short while,
however, Thread2
reports that it has been aborted,
and then it reports that it is exiting. A little while later,
Thread1
reports that it was interrupted. Because
the interrupt waits for the thread to be in a wait state, this can be
a bit less immediate than a call to Abort
. The two
remaining threads continue until they are done. They then exit
naturally, and the main thread, which was joined on all three,
resumes to print its exit message.
At times, you might want to control access to a resource, such as an object’s properties or methods, so that only one thread at a time can modify or use that resource. Your object is similar to the airplane restroom discussed earlier, and the various threads are like the people waiting in line. Synchronization is provided by a lock on the object, which helps the developer avoid having a second thread barge in on your object until the first thread is finished with it.
This section examines three synchronization mechanisms: the
Interlock
class, the
C# lock
statement, and the Monitor
class. But
first, you need to create a shared resource, (often a file or
printer); in this case a simple integer variable:
counter
. You will increment
counter
from each of two threads.
To start, declare the member variable and initialize it to 0:
int counter = 0;
Modify the Incrementer
method to increment the
counter
member variable:
public void Incrementer() { try { while (counter < 1000) { int temp = counter; temp++; // increment // simulate some work in this method Thread.Sleep(1); // assign the Incremented value // to the counter variable // and display the results counter = temp; Console.WriteLine( "Thread {0}. Incrementer: {1}", Thread.CurrentThread.Name, counter); } }
The idea here is to simulate the work that might be done with a
controlled resource. Just as you might open a file, manipulate its
contents, and then close it, here you read the value of
counter
into a temporary variable, increment the
temporary variable, sleep for one millisecond to simulate work, and
then assign the incremented value back to counter
.
The problem is that your first thread reads the value of
counter
(0) and assigns that to a temporary
variable. It then increments the temporary variable. While it is
doing its work, the second thread reads the value of
counter
(still 0) and assigns that value to a
temporary variable. The first thread finishes its work, then assigns
the temporary value (1) back to counter
and
displays it. The second thread does the same. What is printed is
1,1
. In the next go around, the same thing
happens. Rather than having the two threads count
1,2,3,4
, you’ll see
1,2,3,3,4,4
. Example 20-3 shows
the complete source code and output for this
example.
Example 20-3. Simulating a shared resource
#region Using directives using System; using System.Collections.Generic; using System.Text; using System.Threading; #endregion namespace SharedResource { classTester { private int counter = 0; static void Main( ) { // make an instance of this class Tester t = new Tester( ); // run outside static Main t.DoTest( ); } public void DoTest( ) { Thread t1 = new Thread( new ThreadStart( Incrementer ) ); t1.IsBackground = true; t1.Name = "ThreadOne"; t1.Start( ); Console.WriteLine( "Started thread {0}", t1.Name ); Thread t2 = new Thread( new ThreadStart( Incrementer ) ); t2.IsBackground = true; t2.Name = "ThreadTwo"; t2.Start( ); Console.WriteLine( "Started thread {0}", t2.Name ); t1.Join( ); t2.Join( ); // after all threads end, print a message Console.WriteLine( "All my threads are done." ); } // demo function, counts up to 1K public void Incrementer( ) { try { while ( counter < 1000 ) { int temp = counter; temp++; // increment // simulate some work in this method Thread.Sleep( 1 ); // assign the decremented value // and display the results counter = temp; Console.WriteLine( "Thread {0}. Incrementer: {1}", Thread.CurrentThread.Name, counter ); } } catch ( ThreadInterruptedException ) { Console.WriteLine( "Thread {0} interrupted! Cleaning up...", Thread.CurrentThread.Name ); } finally { Console.WriteLine( "Thread {0} Exiting. ", Thread.CurrentThread.Name ); } } } } Output: Started thread ThreadOne Started thread ThreadTwo Thread ThreadOne. Incrementer: 1 Thread ThreadOne. Incrementer: 2 Thread ThreadOne. Incrementer: 3 Thread ThreadTwo. Incrementer: 3 Thread ThreadTwo. Incrementer: 4 Thread ThreadOne. Incrementer: 4 Thread ThreadTwo. Incrementer: 5 Thread ThreadOne. Incrementer: 5 Thread ThreadTwo. Incrementer: 6 Thread ThreadOne. Incrementer: 6
The CLR provides a number of
synchronization mechanisms. These
include the common synchronization tools such as critical sections
(called locks in .NET), as well as the
Monitor
class. Each is discussed later in this
chapter.
Incrementing and decrementing a value is such a common programming
pattern, and one which so often needs synchronization protection,
that the CLR offers a special class,
Interlocked
, just for this purpose.
Interlocked
has two methods,
Increment
and
Decrement
, which not only increment or decrement a
value, but also do so under synchronization control.
Modify the Incrementer
method from Example 20-3 as follows:
public void Incrementer( )
{
try
{
while (counter < 1000)
{int temp = Interlocked.Increment(ref counter);
// simulate some work in this method
Thread.Sleep(0);
// display the incremented value
Console.WriteLine(
"Thread {0}. Incrementer: {1}",
Thread.CurrentThread.Name,
temp);
}
}
}
The catch
and finally
blocks
and the remainder of the program are unchanged from the previous
example.
Interlocked.Increment()
expects a single
parameter: a reference to an int
. Because
int
values are passed by value, use the
ref
keyword, as described in Chapter 4.
The Increment( )
method is overloaded and can take
a reference to a long
rather than to an
int
, if that is what you need.
Once this change is made, access to the counter
member is synchronized, and the output is what we’d
expect:
Output (excerpts): Started thread ThreadOne Started thread ThreadTwo Thread ThreadOne. Incrementer: 1 Thread ThreadTwo. Incrementer: 2 Thread ThreadOne. Incrementer: 3 Thread ThreadTwo. Incrementer: 4 Thread ThreadOne. Incrementer: 5 Thread ThreadTwo. Incrementer: 6 Thread ThreadOne. Incrementer: 7 Thread ThreadTwo. Incrementer: 8 Thread ThreadOne. Incrementer: 9 Thread ThreadTwo. Incrementer: 10 Thread ThreadOne. Incrementer: 11 Thread ThreadTwo. Incrementer: 12 Thread ThreadOne. Incrementer: 13 Thread ThreadTwo. Incrementer: 14 Thread ThreadOne. Incrementer: 15 Thread ThreadTwo. Incrementer: 16 Thread ThreadOne. Incrementer: 17 Thread ThreadTwo. Incrementer: 18 Thread ThreadOne. Incrementer: 19 Thread ThreadTwo. Incrementer: 20
Although
the
Interlocked
object
is fine if you want to increment or decrement a value, there will be
times when you want to control access to other objects as well. What
is needed is a more general synchronization mechanism. This is
provided by the C# lock
feature.
A lock
marks a critical section of your code,
providing synchronization to an object you designate while the lock
is in effect. The syntax of using a lock
is to
request a lock on an object and then to execute a statement or block
of statements. The lock is removed at the end of the statement block.
C# provides direct support for locks through the
lock
keyword. Pass in a reference to an
object, and follow the keyword with a statement block:
lock(expression) statement-block
For example, you can modify Incrementer
again to
use a lock
statement, as follows:
public void Incrementer() { try { while (counter < 1000) { int temp; lock (this) { temp = counter; temp ++; Thread.Sleep(1); counter = temp; } // assign the decremented value // and display the results Console.WriteLine( "Thread {0}. Incrementer: {1}", Thread.CurrentThread.Name, temp); } }
The catch
and finally
blocks
and the remainder of the program are unchanged from the previous
example.
The output from this code is identical to that produced using
Interlocked
.
The objects used so far will be sufficient for most needs. For the most sophisticated control over resources, you might want to use a monitor. A monitor lets you decide when to enter and exit the synchronization, and it lets you wait for another area of your code to become free.
When you want to begin synchronization, call
the
Enter()
method of
the monitor, passing in the object you want to lock:
Monitor.Enter(this);
If the monitor is unavailable, the object protected by the monitor is
presumed to be in use. You can do other work while you wait for the
monitor to become available and then try again. You can also
explicitly choose to Wait( )
, suspending your
thread until the moment the monitor is free and the developer calls
Pulse
(discussed in a bit).
Wait()
helps you control thread ordering.
For example, suppose you are downloading and printing an article from the Web. For efficiency, you’d like to print in a background thread, but you want to ensure that at least 10 pages have downloaded before you begin.
Your printing thread will wait until the get-file thread signals that
enough of the file has been read. You don’t want to
Join
the get-file thread because the file might be
hundreds of pages. You don’t want to wait until it
has completely finished downloading, but you do want to ensure that
at least 10 pages have been read before your print thread begins. The
Wait( )
method is just the ticket.
To simulate this, rewrite Tester
, and add back the
decrementer method. Your incrementer counts up to 10. The decrementer
method counts down to zero. It turns out you don’t
want to start decrementing unless the value of
counter
is at least 5
.
In decrementer
, call Enter
on
the monitor. Then check the value of counter
, and
if it is less than 5
, call Wait
on the monitor:
if (counter < 5) { Monitor.Wait(this); }
This call to Wait( )
frees the monitor, but signals
the CLR that you want the monitor back the next time it is free.
Waiting threads are notified of a chance to run again if the active
thread calls
Pulse( )
:
Monitor.Pulse(this);
Pulse()
signals the CLR that there has been a
change in state that might free a thread that is waiting.
When a thread is finished with the monitor, it must mark the end of
its controlled area of code with a call to
Exit()
:
Monitor.Exit(this);
Example 20-4 continues the simulation, providing
synchronized access to a counter
variable using a
Monitor
.
Example 20-4. Using a Monitor object
#region Using directives using System; using System.Collections.Generic; using System.Text; using System.Threading; #endregion namespace UsingAMonitor { classTester { private long counter = 0; static void Main( ) { // make an instance of this class Tester t = new Tester( ); // run outside static Main t.DoTest( ); } public void DoTest( ) { // create an array of unnamed threads Thread[] myThreads = { new Thread( new ThreadStart(Decrementer) ), new Thread( new ThreadStart(Incrementer) ) }; // start each thread int ctr = 1; foreach ( Thread myThread in myThreads ) { myThread.IsBackground = true; myThread.Start( ); myThread.Name = "Thread" + ctr.ToString( ); ctr++; Console.WriteLine( "Started thread {0}", myThread.Name ); Thread.Sleep( 50 ); } // wait for all threads to end before continuing foreach ( Thread myThread in myThreads ) { myThread.Join( ); } // after all threads end, print a message Console.WriteLine( "All my threads are done." ); } void Decrementer( ) { try { // synchronize this area of code Monitor.Enter( this ); // if counter is not yet 10 // then free the monitor to other waiting // threads, but wait in line for your turn if ( counter < 10 ) { Console.WriteLine( "[{0}] In Decrementer. Counter: {1}. Gotta Wait!", Thread.CurrentThread.Name, counter ); Monitor.Wait( this ); } while ( counter > 0 ) { long temp = counter; temp--; Thread.Sleep( 1 ); counter = temp; Console.WriteLine( "[{0}] In Decrementer. Counter: {1}. ", Thread.CurrentThread.Name, counter ); } } finally { Monitor.Exit( this ); } } void Incrementer( ) { try { Monitor.Enter( this ); while ( counter < 10 ) { long temp = counter; temp++; Thread.Sleep( 1 ); counter = temp; Console.WriteLine( "[{0}] In Incrementer. Counter: {1}", Thread.CurrentThread.Name, counter ); } // I'm done incrementing for now, let another // thread have the Monitor Monitor.Pulse( this ); } finally { Console.WriteLine( "[{0}] Exiting...", Thread.CurrentThread.Name ); Monitor.Exit( this ); } } } } Output: Started thread Thread1 [Thread1] In Decrementer. Counter: 0. Gotta Wait! Started thread Thread2 [Thread2] In Incrementer. Counter: 1 [Thread2] In Incrementer. Counter: 2 [Thread2] In Incrementer. Counter: 3 [Thread2] In Incrementer. Counter: 4 [Thread2] In Incrementer. Counter: 5 [Thread2] In Incrementer. Counter: 6 [Thread2] In Incrementer. Counter: 7 [Thread2] In Incrementer. Counter: 8 [Thread2] In Incrementer. Counter: 9 [Thread2] In Incrementer. Counter: 10 [Thread2] Exiting... [Thread1] In Decrementer. Counter: 9. [Thread1] In Decrementer. Counter: 8. [Thread1] In Decrementer. Counter: 7. [Thread1] In Decrementer. Counter: 6. [Thread1] In Decrementer. Counter: 5. [Thread1] In Decrementer. Counter: 4. [Thread1] In Decrementer. Counter: 3. [Thread1] In Decrementer. Counter: 2. [Thread1] In Decrementer. Counter: 1. [Thread1] In Decrementer. Counter: 0. All my threads are done.
In this
example, decrementer
is started first. In the
output you see Thread1
(the decrementer) start up
and then realize that it has to wait. You then see
Thread2
start up. Only when
Thread2
pulses does Thread1
begin its work.
Try some experiments with this code. First, comment out the call to
Pulse()
. You’ll find that
Thread1
never resumes. Without
Pulse()
, there is no signal to the waiting
threads.
As a second experiment, rewrite Incrementer
to
pulse and exit the monitor after each increment:
void Incrementer() { try { while (counter < 10) { Monitor.Enter(this); long temp = counter; temp++; Thread.Sleep(1); counter = temp; Console.WriteLine( "[{0}] In Incrementer. Counter: {1}", Thread.CurrentThread.Name, counter);Monitor.Pulse(this); Monitor.Exit(this); }
Rewrite Decrementer
as well, changing the
if
statement to a while
statement and knocking down the value from 10
to
5
:
//if (counter < 10) while (counter < 5)
The net effect of these two changes is to cause
Thread2
, the Incrementer
, to
pulse the Decrementer
after each increment. While
the value is smaller than five, the Decrementer
must continue to wait; once the value goes over five, the
Decrementer
runs to completion. When it is done,
the Incrementer
thread can run again. The output
is shown
here:
[Thread2] In Incrementer. Counter: 2 [Thread1] In Decrementer. Counter: 2. Gotta Wait! [Thread2] In Incrementer. Counter: 3 [Thread1] In Decrementer. Counter: 3. Gotta Wait! [Thread2] In Incrementer. Counter: 4 [Thread1] In Decrementer. Counter: 4. Gotta Wait! [Thread2] In Incrementer. Counter: 5 [Thread1] In Decrementer. Counter: 4. [Thread1] In Decrementer. Counter: 3. [Thread1] In Decrementer. Counter: 2. [Thread1] In Decrementer. Counter: 1. [Thread1] In Decrementer. Counter: 0. [Thread2] In Incrementer. Counter: 1 [Thread2] In Incrementer. Counter: 2 [Thread2] In Incrementer. Counter: 3 [Thread2] In Incrementer. Counter: 4 [Thread2] In Incrementer. Counter: 5 [Thread2] In Incrementer. Counter: 6 [Thread2] In Incrementer. Counter: 7 [Thread2] In Incrementer. Counter: 8 [Thread2] In Incrementer. Counter: 9 [Thread2] In Incrementer. Counter: 10
The .NET library provides sufficient thread support that you will rarely find yourself creating your own threads and managing synchronization manually.
Thread synchronization can be tricky, especially in complex programs. If you do decide to create your own threads, you must confront and solve all the traditional problems of thread synchronization, such as race conditions and deadlock.
A race condition exists when the success of your program depends on the uncontrolled order of completion of two independent threads.
Suppose, for example, that you have two threads—one is responsible for opening a file and the other is responsible for writing to the file. It is important that you control the second thread so that it’s assured that the first thread has opened the file. If not, under some conditions, the first thread will open the file and the second thread will work fine; under other unpredictable conditions, the first thread won’t finish opening the file before the second thread tries to write to it, and you’ll throw an exception (or worse, your program will simply seize up and die). This is a race condition, and race conditions can be very difficult to debug.
You can’t leave these two threads to operate
independently; you must ensure that Thread1
will
have completed before Thread2
begins. To
accomplish this, you might
Join( )
Thread2
on Thread1
. As an
alternative, you can use a Monitor
and
Wait( )
for the appropriate conditions before
resuming Thread2
.
When you wait for a resource to become free, you are at risk of deadlock, also called a deadly embrace. In a deadlock, two or more threads are waiting for each other, and neither can become free.
Suppose you have two threads, ThreadA
and
ThreadB
. ThreadA
locks down an
Employee
object and then tries to get a lock on a
row in the database. It turns out that ThreadB
already has that row locked, so ThreadA
waits.
Unfortunately, ThreadB
can’t
update the row until it locks down the Employee
object, which is already locked down by ThreadA
.
Neither thread can proceed, and neither thread will unlock its own
resource. They are waiting for each other in a deadly embrace.
As described, the deadlock is fairly easy to spot—and to
correct. In a program running many threads, deadlock can be very
difficult to diagnose, let alone solve. One guideline is to get all
the locks you need or to release all the locks you have. That is, as
soon as ThreadA
realizes that it
can’t lock the Row
, it should
release its lock on the Employee
object.
Similarly, when ThreadB
can’t
lock the Employee
, it should release the
Row
. A second important guideline is to lock as
small a section of code as possible and to hold the lock as briefly
as possible.
18.218.31.165