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 prevents a second thread from barging in on your object until the first thread is finished with it.
In this section you’ll examine three synchronization mechanisms
provided by the CLR: the Interlock
class, the C#
lock statement, and the Monitor
class. But first,
you’ll need to simulate a shared resource, such as a file or
printer, with a simple integer variable: counter
.
Rather than opening the file or accessing the printer, you’ll
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 we might open a file, manipulate its
contents, and then close it, here we 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 this: your first thread will read the value of counter
(0) and assign that to a temporary variable. It will then increment
the temporary variable. While it is doing its work, the second thread
will read the value of counter (still 0) and assign 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
, we see
1,1,2,2,3,3
. Example 20-3 shows
the complete source code and output for this example.
Example 20-3. Simulating a shared resource
namespace Programming_CSharp
{
using System;
using System.Threading;
class Tester
{
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
Assume your two threads are accessing a database record rather than reading a member variable. For example, your code might be part of an inventory system for a book retailer. A customer asks if Programming C# is available. The first thread reads the value and finds that there is one book on hand. The customer wants to buy the book, so the thread proceeds to gather credit card information and validate the customer’s address.
While this is happening, a second thread asks if this wonderful book is still available. The first thread has not yet updated the record, so one book still shows as available. The second thread begins the purchase process. Meanwhile, the first thread finishes and decrements the counter to zero. The second thread, blissfully unaware of the activity of the first, also sets the value back to zero. Unfortunately, you have now sold the same copy of the book twice.
As noted earlier, you need to synchronize access to the
counter
object (or to the database record, file,
printer, etc.).
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 more sophisticated
tools such as a 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 C# 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 which do so under
synchronization control.
Modify the Incrementer
method from Example 20-3 as follows:
public void Incrementer( )
{
try
{
while (counter < 1000)
{
Interlocked.Increment(ref counter);
// simulate some work in this method
Thread.Sleep(1);
// assign the decremented value
// and display the results
Console.WriteLine(
"Thread {0}. Incrementer: {1}",
Thread.CurrentThread.Name,
counter);
}
}
}
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, you use the
ref
keyword, as described in Chapter 4.
The Increment( )
method is overloaded and can take
a reference to a long
, rather than a reference to
an int
, if that is more convenient.
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 .NET Lock
object.
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. You pass in a reference object and
follow the keyword with a statement block:
lock(expression) statement-block
For example, you can modify Incrementer
once again
to use a lock statement, as follows:
public void Incrementer( ) { try { while (counter < 1000) { lock (this) { int 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, counter); } }
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.
A monitor acts as a smart lock on a resource. When you want to begin
synchronization, you 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
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. 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, you will rewrite tester and add back the
decrementer method. Your incrementer will count up to 10. The
decrementer method will count down to zero. It turns out you
don’t want to start decrementing unless the value of
counter
is at least 5
.
In decrementer
you call Enter
on the monitor. You then check the value of
counter
, and if it is less than
5
, you 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 will be 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. The CLR
will keep track of the fact that the earlier thread asked to wait,
and threads will be guaranteed access in the order in which the waits
were requested. (“Your wait is important to us and will be
handled in the order received.”)
When a thread is finished with the monitor, it can 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
namespace Programming_CSharp
{
using System;
using System.Threading;
class Tester
{
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);
}
}
private long counter = 0;
}
}
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
3.145.18.101