
modern-multithreading-c-java
.pdfSEMAPHORES AND LOCKS IN PTHREADS |
135 |
const int capacity = 3; class Buffer { private:
int buffer[capacity]; int count, in, out;
public:
Buffer() : in(0), out(0), count(0) { } int size() { return count;}
int withdraw () { int value = 0;
value = buffer[out]; // out is shared by Consumers out = (out + 1) % capacity;
count--; return value;
}
void deposit (int value) {
buffer[in] = value; // in is shared by Producers in = (in + 1) % capacity;
count++;
}
};
Buffer sharedBuffer; // 3-slot buffer mutexLock mutexD, mutexW; countingSemaphore emptySlots(capacity); countingSemaphore fullSlots(0);
class Producer : public Thread { public:
virtual void* run () { int i;
std::cout << "producer running" << std::endl; for (i=0; i<2; i++) {
emptySlots.P();
mutexD.lock();
sharedBuffer.deposit(i);
std::cout << "Produced: " << i << std::endl; mutexD.unlock();
fullSlots.V();
}
return 0;
}
};
Listing 3.26 Win32 bounded buffer using countingSemaphores and mutexLocks.

136 |
SEMAPHORES AND LOCKS |
class Consumer : public Thread { public:
virtual void* run () { int result;
std::cout << "consumer running" << std::endl; for (int i=0; i<2; i++) {
fullSlots.P();
mutexW.lock();
result = sharedBuffer.withdraw(); mutexW.unlock();
std::cout << "Consumed: " << result << std::endl; emptySlots.V();
}
return 0;
}
};
int main() {
std::auto_ptr<Producer> p1(new Producer); std::auto_ptr<Producer> p2(new Producer); std::auto_ptr<Consumer> c1(new Consumer); std::auto_ptr<Consumer> c2(new Consumer); p1->start();c1->start(); p2->start();c2->start(); p1->join(); p2->join(); c1->join(); c2->join(); return(0);
}
Listing 3.26 (continued )
3.8.1 Mutex
A Pthreads mutex is a lock with behavior similar to that of a Win32 CRITICAL SECTION. Operations pthread mutex lock() and pthread mutex unlock() are analogous to EnterCriticalSection() and LeaveCriticalSection(), respectively:
žA thread that calls pthread mutex lock() 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 pthread mutex lock() on a mutex and is granted access to the mutex becomes the owner of the mutex.
žA thread releases its ownership by calling pthread mutex unlock(). A thread calling pthread mutex unlock() must be the owner of the mutex.
ž There is a conditional wait operation pthread mutex trylock (pthread mutex t* mutex) that will never block the calling thread. If the mutex is currently locked, the operation returns immediately with the error code EBUSY. Otherwise, the calling thread becomes the owner.

SEMAPHORES AND LOCKS IN PTHREADS |
137 |
Listing 3.27 shows how to use a Pthreads mutex. You initialize a mutex by calling the pthread mutex init() function. The first parameter is the address of the mutex. If you need to initialize a mutex with nondefault attributes, the second parameter can specify the address of an attribute object. When the mutex is no longer needed, it is destroyed by calling pthread mutex destroy().
When you declare a static mutex with default attributes, you can use the PTHREAD MUTEX INITIALIZER macro instead of calling pthread mutex int(). In Listing 3.27, we could have written
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
You do not need to destroy a mutex that was initialized using the PTHREAD MUTEX INITIALIZER macro.
By default, a Pthreads mutex is not recursive, which means that a thread should not try to lock a mutex that it already owns. However, the POSIX 1003.1 2001 standard allows a mutex’ s type attribute to be set to recursive:
pthread_mutex_t mutex; pthread_mutexattr_t mutexAttribute;
int status = pthread_mutexattr_init (&mutexAttribute); if (status !=0) { /* ... */ }
status = pthread_mutexattr_settype(&mutexAttribute, PTHREAD_MUTEX_RECURSIVE);
if (status != 0) { /* ... */}
status = pthread_mutex_init(&mutex,&mutexAttribute); if (status != 0) { /* ... */ }
If a thread that owns a recursive mutex tries to lock the mutex again, the thread is granted access immediately. Before another thread can become the owner, an owning thread must release a recursive mutex the same number of times that it requested ownership.
3.8.2 Semaphore
POSIX semaphores are counting semaphores. Operations sem wait() and
sem post() are equivalent to P() and V(), respectively. POSIX semaphores have the following properties:
žA semaphore is not considered to be owned by a thread—one thread can execute sem wait() on a semaphore and another thread can execute sem post().
žWhen a semaphore is created, the initial value of the semaphore is specified. The initial value must be greater than or equal to zero and less than or equal to the value SEM VALUE MAX.
138 |
SEMAPHORES AND LOCKS |
#include <pthread.h> pthread_mutex_t mutex;
void* Thread1(void* arg) { pthread_mutex_lock(&mutex); /* critical section */ pthread_mutex_unlock(&mutex); return NULL;
}
void* Thread2(void* arg) { pthread_mutex_lock(&mutex); /* critical section */ pthread_mutex_unlock(&mutex); return NULL;
}
int main() { |
|
pthread_t threadArray[2]; |
// array of thread IDs |
int status; |
// error code |
pthread_attr_t threadAttribute; |
// thread attribute |
// initialize mutex
status = pthread_mutex_init(&mutex,NULL);
if (status != 0) { /* See Listing 1.4 for error handling */ } // initialize the thread attribute object
status = pthread_attr_init(&threadAttribute); if (status != 0) { /* ... */}
// set the scheduling scope attribute
status = pthread_attr_setscope(&threadAttribute, PTHREAD_SCOPE_SYSTEM);
if (status != 0) { /* ... */}
// Create two threads and store their IDs in array threadArray
status = pthread_create(&threadArray[0], &threadAttribute, Thread1, (void*) 1L);
if (status != 0) { /* ... */}
status = pthread_create(&threadArray[1], &threadAttribute, Thread2, (void*) 2L);
if (status != 0) { /* ... */}
status = pthread_attr_destroy(&threadAttribute); // destroy attribute object if (status != 0) { /* ... */}
Listing 3.27 Using Pthreads mutex objects.

SEMAPHORES AND LOCKS IN PTHREADS |
139 |
// Wait for threads to finish
status = pthread_join(threadArray[0],NULL); if (status != 0) { /* ... */}
status = pthread_join(threadArray[1],NULL); if (status != 0) { /* ... */}
// Destroy mutex
status = pthread_mutex_destroy(&mutex); if (status != 0) { /* ... */ }
}
Listing 3.27 (continued )
žSemaphore operations follow a different convention for reporting errors. They return 0 for success. On failure, they return a value of −1 and store the appropriate error number into errno. We use the C function perror(const char* string) to transcribe the value of errno into a string and print that string to stderr.
žThere is a conditional wait operation sem trywait (sem t* sem) that will never block the calling thread. If the semaphore value is greater than 0, the value is decremented and the operation returns immediately. Otherwise, the operation returns immediately with the error code EAGAIN indicating that the semaphore value was not greater than 0.
Listing 3.28 shows how Pthreads semaphore objects are used. Header file <semaphore.h> must be included to use the semaphore operations. Semaphores are of the type sem t. A semaphore is created by calling the sem init() function. The first argument is the address of the semaphore. If the second argument has a nonzero value, the semaphore can be shared between processes. With a zero value, it can be shared only between threads in the same process. The third argument is the initial value. When the semaphore is no longer needed, it is destroyed by calling sem destroy().
We can create a simple C++ class that wraps POSIX semaphores, just as we did with Win32 semaphores. Listing 3.29 shows wrapper class POSIXSemaphore. The methods of PthreadSemaphore forward calls to the corresponding POSIX semaphore functions. To assist with testing and debugging, we’ll need user-level lock and semaphore classes like the ones we developed for Win32. We can use POSIXSemaphore to implement both of these classes. The code for C++/Pthreads classes mutexLock and countingSemaphore is identical to the code in Listings 3.23 and 3.24, respectively, except that class win32Semaphore should be replaced by class POSIXSemaphore. The difference between Win32 and POSIX is encapsulated in the semaphore classes.
140 |
SEMAPHORES AND LOCKS |
#include <pthread.h> #include <semaphore.h> #include <stdio.h>
sem_t s;
void* Thread1(void* arg) { int status;
status = sem_wait(&s); if (status !=0) {
std::cout << __FILE__ << ":" << __LINE__ << "- " << flush; perror("sem_wait failed"); exit(status);
}
/* critical section */ status = sem_post(&s); if (status !=0) {
std::cout << __FILE__ << ":" << __LINE__ << "- " << flush; perror("sem_post failed"); exit(status);
}
return NULL; // implicit call to pthread_exit(NULL);
}
void* Thread2(void* arg) { int status;
status = sem_wait(&s); if (status !=0) {
std::cout << __FILE__ << ":" << __LINE__ << "- " << flush; perror("sem_wait failed"); exit(status);
}
/* critical section */ status = sem_post(&s); if (status !=0) {
std::cout << __FILE__ << ":" << __LINE__ << "- " << flush; perror("sem_post failed"); exit(status);
}
return NULL; // implicit call to pthread_exit(NULL);
}
int main() { |
|
pthread_t threadArray[2]; |
// array of thread IDs |
int status; |
// error code |
pthread_attr_t threadAttribute; |
// thread attribute |
Listing 3.28 Using POSIX semaphore objects.
ANOTHER NOTE ON SHARED MEMORY CONSISTENCY |
141 |
//initialize semaphore s status = sem_init(&s,0,1); if (status !=0) {
std::cout << __FILE__ << ":" << __LINE__ << "- " << flush; perror("sem_init failed"); exit(status);
}
//initialize the thread attribute object
status = pthread_attr_init(&threadAttribute);
if (status != 0) { /* see Listing 1.4 for Pthreads error handling */} // set the scheduling scope attribute
status = pthread_attr_setscope(&threadAttribute, PTHREAD_SCOPE_SYSTEM);
if (status != 0) { /* ... */}
// Create two threads and store their IDs in array threadArray
status = pthread_create(&threadArray[0], &threadAttribute, Thread1, (void*) 1L);
if (status != 0) { /* ... */}
status = pthread_create(&threadArray[1], &threadAttribute, Thread2, (void*) 2L);
if (status != 0) { /* ... */}
status = pthread_attr_destroy(&threadAttribute); // destroy the attribute object if (status != 0) { /* ... */}
// Wait for threads to finish
status = pthread_join(threadArray[0],NULL); if (status != 0) { /* ... */}
status = pthread_join(threadArray[1],NULL); if (status != 0) { /* ... */}
// Destroy semaphore s status = sem_destroy(&s); if (status !=0) {
std::cout << __FILE__ << ":" << __LINE__ << "- " << flush; perror("sem_destroy failed"); exit(status);
}
}
Listing 3.28 (continued )
3.9 ANOTHER NOTE ON SHARED MEMORY CONSISTENCY
Recall from Section 2.5.6 the issues surrounding shared memory consistency. Compiler and hardware optimizations may reorder read and write operations on shared variables, making it difficult to reason about the behavior of multithreaded
142 |
SEMAPHORES AND LOCKS |
#include <pthread.h> #include <semaphore.h> #include <stdio.h> #include <iostream>
const int maxDefault = 999; class POSIXSemaphore { private:
sem_t s; int permits;
public:
void P(); void V();
POSIXSemaphore(int initial); ~POSIXSemaphore();
};
POSIXSemaphore::POSIXSemaphore (int initial) : permits(initial) { // assume semaphore is accessed by the threads in a single process int status = sem_init(&s, 0, initial);
if (status !=0) {
std::cout << __FILE__ << ":" << __LINE__ << "- " << flush; perror("sem_init failed"); exit(status);
}
}
POSIXSemaphore:: ~ POSIXSemaphore () { int status = sem_destroy(&s);
if (status !=0) {
std::cout << __FILE__ << ":" << __LINE__ << "- " << flush; perror("sem_destroy failed"); exit(status);
}
}
void POSIXSemaphore::P() { int status = sem_wait(&s); if (status !=0) {
std::cout << __FILE__ << ":" << __LINE__ << "- " << flush; perror("sem_wait failed"); exit(status);
}
}
void POSIXSemaphore::V() { int status = sem_post(&s); if (status !=0) {
std::cout << __FILE__ << ":" << __LINE__ << "- " << flush; perror("sem_post failed"); exit(status);
}
}
Listing 3.29 Class POSIXSemaphore.
TRACING, TESTING, AND REPLAY FOR SEMAPHORES AND LOCKS |
143 |
programs. Fortunately, critical sections created using Java’s built-in synchronization operations or the operations in the Win32 or Pthreads library provide mutual exclusion and also protect against unwanted reorderings.
These synchronization operations in Java and in the thread libraries interact with the memory system to ensure that shared variables accessed in critical sections have values that are consistent across threads. For example, the shared variable values that a thread can see when it unlocks a mutex can also be seen by any thread that later locks the same mutex. Thus, an execution in which shared variables are correctly protected by locks or semaphores is guaranteed to be sequentially consistent. This guarantee allows us to ignore shared memory consistency issues when we use locks and semaphores to create critical sections in our programs. For this reason, we make it a rule always to access shared variables inside critical sections. Next we will see that this rule also simplifies testing and debugging.
3.10 TRACING, TESTING, AND REPLAY FOR SEMAPHORES AND LOCKS
In this section we address two testing and debugging issues for programs that use semaphores and locks. First, we describe a special testing technique for detecting violations of mutual exclusion. Then we show how to trace and replay program executions during debugging.
3.10.1 Nondeterministic Testing with the Lockset Algorithm
A concurrent program that uses semaphores and locks can be tested for data races. Recall from Chapter 1 that a data race is a failure to correctly implement critical sections for nonatomic shared variable accesses. The approach we will use to detect data races is to monitor shared variable accesses and make sure that each variable has been properly locked before it is accessed. Since program executions are nondeterministic, we will need to execute the program several times with the same test input in order to increase our chances of finding data races. This type of testing is called nondeterministic testing.
Nondeterministic testing of a concurrent program CP involves the following steps:
1.Select a set of inputs for CP.
2.For each input X selected, execute CP with X many times and examine the result of each execution.
Multiple, nondeterministic executions of CP with input X may exercise different behaviors of CP and thus may detect more failures than a single execution of CP with input X.
144 |
SEMAPHORES AND LOCKS |
The purpose of nondeterministic testing is to exercise as many distinct program behaviors as possible. Unfortunately, experiments have shown that repeated executions of a concurrent program are not likely to execute different behaviors [Hwang et al. 1995]. In the absence of significant variations in I/O delays or network delays, or significant changes in the system load, programs tend to exhibit the same behavior from execution to execution. Furthermore, the probe effect (see Section 1.7), which occurs when programs are instrumented with debugging code, may make it impossible for some failures to be observed.
There are several techniques we can use to increase the likelihood of exercising different behaviors. One is to change the scheduling algorithm used by the operating system (e.g., change the value of the time quantum that is used for round-robin scheduling). However, in many commercial operating systems this is simply not an option. The second technique is to insert Sleep(t) statements into the program with the sleep amount t randomly chosen. Executing a Sleep statement forces a context switch and thus indirectly affects thread scheduling. We have implemented this second technique as an execution option for programs that use the binarySemaphore, countingSemaphore, and mutexLock classes in our synchronization library. When this option is specified, Sleep statements are executed at the beginning of methods P(), V(), lock(), and unlock(). The Sleep time is randomly chosen within a programmable range. The random delays can be used in conjunction with the tracing and replay functions so that any failures that are observed can also be replayed.
To detect data races, we combine nondeterministic testing with the lockset algorithm [Savage et al. 1997]. The lockset algorithm checks that all shared variables follow a consistent locking discipline in which every shared variable is protected by a lock. Since there is no way of knowing which locks are intended to protect which variables, we must monitor the executions and try to infer a relationship between the locks and variables. For each variable, we determine if there is some lock that is always held whenever the variable is accessed.
For shared variable v, let the set CandidateLocks(v) be those locks that have protected v during the execution so far. Thus, a lock l is in CandidateLocks(v) if, during the execution so far, every thread that has accessed v was holding l at the moment of the access. CandidateLocks(v) is computed as follows:
žWhen a new variable v is initialized, its candidate set is considered to hold all possible locks.
žWhen v is accessed on a read or write operation by T, CandidateLocks(v) is refined. The new value of CandidateLocks(v) is the intersection of CandidateLocks(v) and the set of locks held by thread T.
Based on this refinement algorithm, if some lock l protects v consistently, it will remain in CandidateLocks(v) as CandidateLocks(v) is refined. If CandidateLocks(v) becomes empty, it indicates that there is no lock that protects v consistently. Following is the lockset algorithm: