Добавил:
Upload Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:

modern-multithreading-c-java

.pdf
Скачиваний:
18
Добавлен:
22.05.2015
Размер:
4.77 Mб
Скачать

SEMAPHORES AND LOCKS IN JAVA

115

mutexlock object can be locked in one method and unlocked in another, and one method can contain multiple calls to lock() and unlock(). On the other hand, calls to lock() and unlock() are not made automatically on method entry and exit as they are in synchronized methods.

Member variable owner holds a reference to the current owner of the lock. This reference is obtained by calling Thread.currentThread(), which returns the thread that is currently executing method lock(). The value of member variable free is true if the lock is not owned currently. A thread that calls lock() becomes the owner if the lock is free; otherwise, the calling thread increments waiting and blocks itself by executing wait(). When a thread calls unlock(), if there are any waiting threads, waiting is decremented and one of the waiting threads is notified. The notified thread is guaranteed to become the new owner, since any threads that barge ahead of the notified thread will find that the lock is not free and will block themselves on wait(). Member count tracks the number of times that the owning thread has called lock(). The owning thread must call unlock() the same number of times that it called lock() before another thread can become the owner of the lock.

3.6.3 Class Semaphore

Package java.util.concurrent contains class Semaphore. Class Semaphore is a counting semaphore with operations acquire() and release() instead of P() and V(). The constructor for class Semaphore optionally accepts a fairness parameter. When this fairness parameter is false, class Semaphore makes no guarantees about the order in which threads acquire permits, as barging is permitted. When the fairness parameter is set to true, the semaphore guarantees that threads invoking acquire() obtain permits in FCFS order.

Semaphore mutex = new Semaphore(1,true); // mutex is a FCFS semaphore // initialized to 1

Thread1

Thread2

 

 

mutex.acquire();

mutex.acquire();

/* critical section */

/* critical section */

mutex.release();

mutex.release();

Class Semaphore also provides method tryAcquire(), which acquires a permit only if one is available at the time of invocation. Method tryAcquire() returns true if a permit was acquired and false otherwise.

if (mutex.tryAcquire()) { ... }

A limit can be placed on the amount of time that a thread will wait for a permit:

if ( mutex.tryAcquire(50L, TimeUnit.MILLISECONDS) ) {...}

116

SEMAPHORES AND LOCKS

This call to tryAcquire() acquires a permit from semaphore mutex if one becomes available within 50 milliseconds and the current thread has not been interrupted. Note that a call to the (untimed) tryAcquire() method does not honor the fairness setting. Even when a semaphore has been set to use a fair ordering policy, a call to tryAcquire() will immediately acquire a permit if one is available, whether or not other threads are currently waiting.

3.6.4 Class ReentrantLock

Package java.util.concurrent.locks provides a mutex lock class called RentrantLock with methods lock() and unlock(). Class ReentrantLock has the same behavior and semantics as the implicit monitor lock accessed using synchronized methods and statements. Like class Semaphore, the constructor for class ReentrantLock accepts an optional fairness parameter. When set to true, priority is given to the longest-waiting thread. Class ReentrantLock also provides untimed and timed tryLock() methods that behave the same as the untimed and timed tryAcquire() methods of class Semaphore.

It is recommended that a call to lock() be followed immediately by a try block:

class illustrateTryBlock {

private final ReentrantLock mutex = new ReentrantLock(); public void foo() {

mutex.lock(); try {

/* body of method foo() */

} finally { // ensures that unlock() is executed mutex.unlock()

}

}

}

3.6.5 Example: Java Bounded Buffer

Listing 3.17 is a Java solution to the bounded buffer problem for two producers and two consumers. This solution is based on the solution in Listing 3.6. Since there are multiple producers and consumers, two mutexLocks are used. Lock mutexD provides mutual exclusion among the Producers and lock mutexW provides mutual exclusion among the Consumers. (No mutual exclusion is needed between Producers and Consumers since a Producer and a Consumer can never access the same slot simultaneously.)

A better programming style would be to move the four semaphores and the P() and V() operations that are executed on them into the methods of the Buffer class. We will save this step until Chapter 4, where we present a synchronization construct that better supports the concepts of encapsulation and information hiding.

SEMAPHORES AND LOCKS IN JAVA

117

public final class boundedBuffer {

 

public static void main (String args[]) {

 

final int size = 3;

 

Buffer b = new Buffer(size);

 

mutexLock mutexD = new mutexLock();

// exclusion for Producers

mutexLock mutexW = new mutexLock();

// exclusion for Consumers

countingSemaphore emptySlots = new countingSemaphore(size); countingSemaphore fullSlots = new countingSemaphore(0); Producer p1 = new Producer (b,emptySlots,fullSlots,mutexD,1); Producer p2 = new Producer (b,emptySlots,fullSlots,mutexD,2); Consumer c1 = new Consumer (b,emptySlots,fullSlots,mutexW,1); Consumer c2 = new Consumer (b,emptySlots,fullSlots,mutexW,2); p1.start(); c1.start();

p2.start(); c2.start();

}

}

final class Producer extends Thread { private Buffer b = null;

private int num;

private countingSemaphore emptySlots; private countingSemaphore fullSlots; private mutexLock mutexD;

Producer (Buffer b, countingSemaphore emptySlots, countingSemaphore fullSlots, mutexLock mutexD, int num) {

this.b = b; this.num = num;

this.emptySlots = emptySlots; this.fullSlots = fullSlots; this.mutexD = mutexD;

}

public void run () {

System.out.println ("Producer Running"); for (int i = 0; i < 3; i++) {

emptySlots.P();

mutexD.lock();

b.deposit(i);

System.out.println ("Producer # "+ num + "deposited "+ i); System.out.flush();

mutexD.unlock();

fullSlots.V();

}

}

}

Listing 3.17 Java bounded buffer using semaphores and locks.

118

SEMAPHORES AND LOCKS

final class Consumer extends Thread { private Buffer b;

private int num;

private countingSemaphore emptySlots; private countingSemaphore fullSlots; private mutexLock mutexW;

Consumer (Buffer b, countingSemaphore emptySlots, countingSemaphore fullSlots, mutexLock mutexW, int num) {

this.b = b; this.num = num;

this.emptySlots = emptySlots; this.fullSlots = fullSlots; this.mutexW = mutexW;

}

public void run () {

System.out.println ("Consumer running"); int value = 0;

for (int i = 0; i < 3; i++) { fullSlots.P(); mutexW.lock(); value = b.withdraw();

System.out.println ("Consumer # "+ num + "withdrew "+ value); System.out.flush();

mutexW.unlock();

emptySlots.V();

}

}

}

final class Buffer {

private int[] buffer = null; private int in = 0, out = 0; private int capacity;

public Buffer(int capacity) { this.capacity = capacity; buffer = new int[capacity];

}

public int withdraw () { int value = buffer[out];

out = (out + 1) % capacity; // out is shared by consumers return value;

}

public void deposit (int value) { buffer[in] = value;

in = (in + 1) % capacity; // in is shared by producers

}

}

Listing 3.17 (continued )

SEMAPHORES AND LOCKS IN Win32

119

3.7 SEMAPHORES AND LOCKS IN Win32

Win32 provides four types of objects that can be used for thread synchronization:

žMutex

žCRITICAL SECTION

žSemaphore

žEvent

Mutex and CRITICAL SECTION objects are Win32 versions of the lock objects we have been using, while Win32 Semaphores are counting semaphores. (Unfortunately, Win32 uses “CRITICAL SECTION” as the name of the lock instead of the name of the code segment that the lock protects.) Semaphore, Event, and Mutex objects can be used to synchronize threads in different processes or threads in the same process, but CRITICAL SECTION objects can only be used to synchronize threads in the same process.

3.7.1 CRITICAL SECTION

A CRITICAL SECTION is a lock object that can be used to synchronize threads in a single process. A CRITICAL SECTION is essentially a Win32 version of the recursive mutexLock object described in Section 3.3:

žA thread that calls EnterCriticalSection() is granted access if no other thread owns the CRITICAL SECTION; otherwise, the thread is blocked.

ž A

thread releases its ownership by calling

LeaveCriticalSection().

A

thread calling LeaveCriticalSection() must

be the owner of the

CRITICAL SECTION. If a thread calls LeaveCriticalSection() when it does not have ownership of the CRITICAL SECTION, an error occurs that may cause another thread using EnterCriticalSection() to wait indefinitely.

žA thread that owns a CRITICAL SECTION and requests access again is granted access immediately. An owning thread must release a CRITICAL SECTION the same number of times that it requested ownership before another thread can become the owner.

Listing 3.18 shows how to use a CRITICAL SECTION. A CRITICAL

SECTION object must be initialized before it

is used and deleted when it

is no longer needed. Listing 3.19 shows class

win32Critical Section, which

 

 

 

 

is a wrapper for CRITICAL SECTION objects. Class win32Critical Section hides the details of using CRITICAL SECTIONs. The methods of win32Critical Section simply forward their calls to the corresponding CRITICAL SECTION functions. The CRITICAL SECTION member cs is initialized when a win32Critical Section object is constructed and deleted when the object is destructed.

120

SEMAPHORES AND LOCKS

CRITICAL_SECTION cs;

// global CRITICAL_SECTION

unsigned WINAPI Thread1(LPVOID lpvThreadParm) { EnterCriticalSection(&cs);

// access critical section LeaveCriticalSection(&cs); return 0;

}

unsigned WINAPI Thread2(LPVOID lpvThreadParm) { EnterCriticalSection(&cs);

// access critical section LeaveCriticalSection(&cs); return 0;

}

int main() {

HANDLE threadArray[2]; unsigned winThreadID; InitializeCriticalSection(&cs);

threadArray[0]= (HANDLE)_beginthreadex(NULL,0, Thread1, NULL,0, &winThreadID );

threadArray[1]= (HANDLE)_beginthreadex(NULL,0, Thread2, NULL,0, &winThreadID );

WaitForMultipleObjects(2,threadArray,TRUE,INFINITE);

CloseHandle(threadArray[0]);

CloseHandle(threadArray[1]);

DeleteCriticalSection(&cs); return 0;

}

Listing 3.18 Using a Win32 CRITICAL SECTION.

class win32Critical_Section {

// simple class to wrap a CRITICAL_SECTION object with lock/unlock operations

private: CRITICAL_SECTION cs;

public:

win32Critical_Section () { InitializeCriticalSection(&cs); }win32Critical_Section () { DeleteCriticalSection(&cs);} void lock() { EnterCriticalSection(&cs);}

void unlock() { LeaveCriticalSection(&cs);}

};

Listing 3.19 C++ class win32Critical Section.

SEMAPHORES AND LOCKS IN Win32

121

Class win32Critical Section can be used to create lockable objects as shown in Section 3.3:

class lockableObject { public:

void F() { mutex.lock();

...; mutex.unlock();

}

void G() { mutex.lock();

...; F(); ...; // method G() calls method F() mutex.unlock();

}

private:

...

win32Critical_Section mutex;

};

A better approach to creating lockable objects is to take advantage of C++ semantics for constructing and destructing the local (automatic) variables of a method. Listing 3.20 shows class template mutexLocker<> whose type parameter lockable specifies the type of lock (e.g., win32Critical Section) that will be used to create a lockable object. The constructor and destructor for mutexLocker are responsible for locking and unlocking the lockable object that is stored as data member aLockable.

To make a method a critical section, begin the method by creating a mutexLocker object. A new lockableObject class is shown below. Methods F() and G() begin by constructing a mutexLocker object called locker, passing locker a win32Critical Section lock named mutex to manage:

class lockableObject { public:

void F() {

mutexLocker< win32Critical_Section > locker(mutex);

...

}

void G() {

mutexLocker< win32Critical_Section > locker(mutex);

...; F(); ...; // this call to F () is inside a critical section

}

private:

...

win32Critical_Section mutex;

};

122

SEMAPHORES AND LOCKS

template<class lockable> class mutexLocker { public:

mutexLocker(lockable& aLockable_) : aLockable(aLockable_) {lock();}mutexLocker() { unlock();}

void lock() {aLockable.lock();} void unlock() {aLockable.unlock();}

private:

lockable& aLockable;

};

Listing 3.20 C++ class template mutexLocker.

The locking and unlocking of mutex will occur automatically as part of the normal allocation and deallocation of local variable locker. When locker is constructed, mute.lock() is called. When locker is destructed, mutex.unlock() is called. It is now impossible to forget to unlock the critical section when leaving F() or G(). Furthermore, the destructor for locker will be called even if an exception is raised.

3.7.2 Mutex

A Mutex is a recursive lock with behavior similar to that of a CRITICAL SECTION. Operations WaitForSingleObject() and ReleaseMutex() are analogous to EnterCriticalSection() and LeaveCriticalSection(), respectively:

ž A thread that calls WaitForSingleObject() on a Mutex is granted access to the Mutex if no other thread owns the Mutex; otherwise, the thread is blocked.

žA thread that calls WaitForSingleObject() on a Mutex and is granted access to the Mutex becomes the owner of the Mutex.

žA thread releases its ownership by calling ReleaseMutex(). A thread calling ReleaseMutex() must be the owner of the Mutex.

žA thread that owns a Mutex and requests access again is immediately granted access. An owning thread must release a Mutex the same number of times that it requested ownership, before another thread can become the owner.

Mutex objects have the following additional features:

žA timeout can be specified on the request to access a Mutex.

žWhen the Mutex is created, there is an argument that specifies whether the thread that creates the Mutex object is to be considered as the initial owner of the object.

Listing 3.21 shows how to use a Mutex object. You create a Mutex by calling the CreateMutex() function. The second parameter indicates whether the thread creating the Mutex is to be considered the initial owner of the Mutex. The last parameter is a name that is assigned to the Mutex.

SEMAPHORES AND LOCKS IN Win32

123

HANDLE hMutex = NULL;

// global mutex

unsigned WINAPI Thread1(LPVOID lpvThreadParm) {

// Request ownership of mutex.

 

DWORD rc =::WaitForSingleObject(

 

hMutex,

// handle to the mutex

INFINITE);

// wait forever (no timeout)

switch(rc) {

 

case WAIT_OBJECT_0:

// wait completed successfully

 

break;

// received ownership

case

WAIT_FAILED:

// wait failed

 

// received ownership but the program’s state is unknown

case

WAIT_ABANDONED:

 

case

WAIT_TIMEOUT: // timeouts impossible since INFINITE used

 

PrintError("WaitForSingleObject failed at ",__FILE__,__LINE__);

// see Listing 1.3 for PrintError(). break;

}

// Release ownership of mutex rc = ::ReleaseMutex(hMutex);

if (!rc) PrintError("ReleaseMutex failed at ",__FILE__,__LINE__);

return 0;

}

unsigned WINAPI Thread2(LPVOID lpvThreadParm) { /* same as Thread1 */

}

int main() {

HANDLE threadArray[2]; unsigned threadID; hMutex = CreateMutex(

NULL,

// no security attributes

FALSE,

// this mutex is not initially owned by the creating thread

NULL);

// unnamed mutex that will not be shared across processes

threadArray[0]= (HANDLE)_beginthreadex(NULL,0, Thread1, NULL,0, &threadID );

threadArray[1]= (HANDLE)_beginthreadex(NULL,0, Thread2, NULL,0, &threadID );

WaitForMultipleObjects(2,threadArray,TRUE,INFINITE); CloseHandle(threadArray[0]); // release references when finished with them CloseHandle(threadArray[1]);

CloseHandle(hMutex); return 0;

}

Listing 3.21 Using Win32 Mutex objects.

124 SEMAPHORES AND LOCKS

Unlike a CRITICAL SECTION, a Mutex object is a kernel object that can be shared across processes. A Mutex’s name can be used in other processes to get the handle of the Mutex by calling CreateMutex() or OpenMutex(). The fact that Mutex objects are kernel objects means that CRITICAL SECTIONS may be faster than Mutexes. If a thread executes EnterCriticalSection() on a CRITICAL SECTION when the CRITICAL SECTION is not owned, an atomic interlocked test is performed (see Sections 2.2.1 and 2.3) and the thread continues without entering the kernel. If the CRITICAL SECTION is already owned by a different thread, the thread enters the kernel and blocks. A call to WaitForSingleObject() on a Mutex always enters the kernel. In practice, the relative performance of CRITICAL SECTIONS and Mutexes depends on several factors [Hart 2000b].

If a thread terminates while owning a Mutex, the Mutex is considered to be abandoned. When this happens, the system will grant ownership of the Mutex to a waiting thread. The thread that becomes the new owner receives a return code of WAIT ABANDONED.

We can wrap a Mutex object inside a C++ class, just as we did for CRITICAL SECTION objects. Instead of showing wrapper class win32Mutex, later we present a C++/Win32/Pthreads version of Java class mutexLock. This custom class guarantees FCFS notifications and can be extended to support tracing, testing, and replay.

3.7.3 Semaphore

Win32 Semaphores are counting semaphores. Operations WaitForSingleObject() and ReleaseSemaphore() are analogous to P() and V(), respectively. When a Semaphore is created, the initial and maximum values of the Semaphore are specified. The initial value must be greater than or equal to zero and less than or equal to the maximum value. The maximum value must be greater than zero. The value of the Semaphore can never be less than zero or greater than the maximum value specified.

HANDLE hSemaphore;

hSemaphore = CreateSemaphore(

NULL,

// no security attributes

1L,

// initial count

LONG_MAX,

// maximum count (defined in C++ as at least 2147483647)

NULL);

// unnamed semaphore

if (!hSemaphore)

 

PrintError("CreateSemaphore",__FILE__,__LINE__);

The last parameter of CreateSemaphore() is a name that is assigned to the Semaphore. Other processes can use this name to get the handle of the Semaphore by calling CreateSemaphore() or OpenSemaphore(). A Semaphore is not considered to be owned by a thread—one thread can execute WaitForSingleObject() on a Semaphore and another thread can call ReleaseSemaphore().

Соседние файлы в предмете [НЕСОРТИРОВАННОЕ]