Figure 10-5
To enable you to create a mutually exclusive lock on a block of code (the code that is locked is called a critical section ), C# provides the lock
keyword. Using it, you can ensure that a block of code runs to completion without any interruption by other threads.
To lock a block of code, give the lock
statement an object as argument. The preceding code could be written as follows:
class Program {
//---used for locking---
static object obj = new object();
//---initial balance amount---
static int balance = 500;
static void Main(string[] args) {
Thread t1 = new Thread(new ThreadStart(Debit));
t1.Start();
Thread t2 = new Thread(new ThreadStart(Credit));
t2.Start();
Console.ReadLine();
}
static void Credit() {
//---credit 1500---
for (int i = 0; i < 15; i++) {
lock (obj) {
balance += 100;
Console.WriteLine("After crediting, balance is {0}", balance);
}
}
}
static void Debit() {
//---debit 1000---
for (int i = 0; i < 10; i++) {
lock (obj) {
balance -= 100;
Console.WriteLine("After debiting, balance is {0}", balance);
}
}
}
}
Notice that you first create an instance of an object that will be used for locking purposes:
//---used for locking---
static object obj = new object();
In general, it is best to avoid using a public object for locking purposes. This prevents situations in which threads are all waiting for a public object, which may itself be locked by some other code.
To delineate a block of code to lock
, enclose the statements with the lock statement:
lock (obj) {
//---place code here---
}
As long as one thread is executing the statements within the block, all other threads will have to wait for the statements to be completed before they can execute the statements.
Figure 10-6 shows one possible outcome of the execution.
Figure 10-6
Notice that the value of balance is now consistent after each credit/debit operation.
The limitation of the lock
statement is that you do not have the capability to release the lock halfway through the critical section. This is important because there are situations in which one thread needs to release the lock so that other threads have a chance to proceed before the first thread can resume its execution.
For instance, you saw in Figure 10-6 that on the fifth line the balance goes into a negative value. In real life this might not be acceptable. The bank might not allow your account to go into a negative balance, and thus you need to ensure that you have a positive balance before any more debiting can proceed. Hence, you need to check the value of balance
. If it is 0, then you should release the lock and let the crediting thread have a chance to increment the balance before you do any more debiting.
For this purpose, you can use the Monitor
class provided by the .NET Framework class library. Monitor
is a static class that controls access to objects by providing a lock. Here's a rewrite of the previous program using the Monitor
class:
class Program {
//---used for locking---
static object obj = new object();
//---initial balance amount---
static int balance = 500;
static void Main(string[] args) {
Thread t1 = new Thread(new ThreadStart(Debit));
t1.Start();
Thread t2 = new Thread(new ThreadStart(Credit));
t2.Start();
Console.ReadLine();
}
static void Credit() {
//---credit 1500---
for (int i = 0; i < 15; i++) {
Monitor.Enter(obj);
balance += 100;
Console.WriteLine("After crediting, balance is {0}", balance);
Monitor.Exit(obj);
}
}
static void Debit() {
//---debit 1000---
for (int i = 0; i < 10; i++) {
Monitor.Enter(obj);
balance -= 100;
Console.WriteLine("After debiting, balance is {0}", balance);
Monitor.Exit(obj);
}
}
}
The Enter()
method of the Monitor
class acquires a lock on the specified object, and the Exit()
method releases the lock. The code enclosed by the Enter()
and Exit()
methods is the critical section. The C# lock
statement looks similar to the Monitor
class; in fact, it is implemented with the Monitor
class. The following lock
statement, for instance:
lock (obj) {
balance -= 100;
Console.WriteLine("After debiting, balance is {0}", balance);
}
Is equivalent to this Monitor
class usage:
Monitor.Enter(obj);
try {
balance -= 100;
Console.WriteLine("After debiting, balance is {0}", balance);
} finally {
Monitor.Exit(obj);
}
Now the code looks promising, but the debiting could still result in a negative balance. To resolve this, you need to so some checking to ensure that the debiting does not proceed until there is a positive balance. Here's how:
static void Debit() {
//---debit 1000---
for (int i = 0; i < 10; i++) {
Monitor.Enter(obj);
if (balance == 0) Monitor.Wait(obj);
balance -= 100;
Console.WriteLine("After debiting, balance is {0}", balance);
Monitor.Exit(obj);
}
}
When you use the Wait()
method of the Monitor
class, you release the lock on the object and enter the object's waiting queue. The next thread that is waiting for the object acquires the lock. If the balance is 0, the debit thread would give up control and let the credit thread have the lock.
However, this code modification may result in the scenario shown in Figure 10-7, in which after debiting the balance five times, balance
becomes 0. On the sixth time, the lock held by the debit thread is released to the credit thread. The credit thread credits the balance 15 times. At that point, the program freezes. Turns out that the credit thread has finished execution, but the debit thread is still waiting for the lock to be explicitly returned to it.
Читать дальше