- Workers of the World
- Lock the Door Behind You
- On One Condition
- Reading, Writing, and Arithmetic
- Loose Threads
Reading, Writing, and Arithmetic
Condition variables are useful for synchronization, and mutexes are good for protecting critical sections. Often, however, you’ll have a resource that several threads want to be able to read at once. This isn’t a problem; as many threads as need to can read a memory location at once, without issue. The problem comes when one thread wants to write to that memory location.
If one thread modifies a shared resource, any other threads that are reading that resource will be in an inconsistent state. The locking model required for this scenario has the following properties:
- Anyone can gain read access to a resource, as long as no one has write access.
- Write access can be granted only when no one has read access.
Again, pthreads come with a built-in mechanism for this situation, in the form of read/write locks. These locks have one extra feature that make it easier to acquire write locks: No one is allowed to gain a read lock when someone is waiting for a write lock. This means that a thread trying to gain write access will block any thread trying to gain read access until it has finished.
The example in Listing 4 again has four threads. The first one fills an array with random numbers. While it’s doing this, it has a write lock. Once the thread has finished, it broadcasts on a condition variable, waking up three other threads. These threads then acquire a read lock and generate the total, average, and maximum value for the array.
Listing 4 calculate.c.
#include <stdio.h> #include <stdlib.h> #include <pthread.h> #include <math.h> #include <unistd.h> #include <limits.h> pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER; pthread_cond_t condition = PTHREAD_COND_INITIALIZER; pthread_rwlock_t lock; #define NUMBERS 100 int numbers[NUMBERS]; int averageRun = 0; int sumRun = 0; int maxRun = 0; void * makerandom(void* spare) { while(1) { pthread_mutex_lock(&mutex); pthread_rwlock_wrlock(&lock); printf("Generating numbers...\n"); for(unsigned int i=0 ; i<NUMBERS ; i++) { numbers[i] = random(); } //Unlock the array when we have finished with it pthread_rwlock_unlock(&lock); //Set a flag so that the other functions run if they miss the condition variable maxRun = averageRun = sumRun = 1; //Wake everyone else up... pthread_cond_broadcast(&condition); //...and let them run pthread_mutex_unlock(&mutex); sleep(1); } } void * sum(void * spare) { while(1) { pthread_mutex_lock(&mutex); //Only wait on the condition variable if there is nothing else to do if(sumRun == 0) { pthread_cond_wait(&condition, &mutex); } //Protect the array while we look at it pthread_rwlock_rdlock(&lock); if(sumRun == 1) { //Unset the flag before releasing the mutex so we don’t miss it being set again sumRun = 0; pthread_mutex_unlock(&mutex); long long total = 0; for(unsigned int i=0 ; i<NUMBERS ; i++) { total += numbers[i]; } printf("Total = %lld\n",total); } else { //Release the mutex if we didn’t do anything with the array pthread_mutex_unlock(&mutex); } sleep(1); //Unlock the array pthread_rwlock_unlock(&lock); } } void * average(void * spare) { while(1) { pthread_mutex_lock(&mutex); if(averageRun == 0) { pthread_cond_wait(&condition, &mutex); } pthread_rwlock_rdlock(&lock); if(averageRun == 1) { averageRun = 0; pthread_mutex_unlock(&mutex); long long total = 0; for(unsigned int i=0 ; i<NUMBERS ; i++) { total += numbers[i]; } printf("Average = %lld\n",total / NUMBERS); } else { pthread_mutex_unlock(&mutex); } pthread_rwlock_unlock(&lock); } } void * max(void * spare) { while(1) { pthread_mutex_lock(&mutex); if(maxRun == 0) { pthread_cond_wait(&condition, &mutex); } pthread_rwlock_rdlock(&lock); if(maxRun == 1) { maxRun = 0; pthread_mutex_unlock(&mutex); int max = -INT_MAX; for(unsigned int i=0 ; i<NUMBERS ; i++) { if(numbers[i] > max) { max = numbers[i]; } } printf("Max = %d\n",max); } else { pthread_mutex_unlock(&mutex); } pthread_rwlock_unlock(&lock); } } int main(void) { pthread_t thread1, thread2; //Dynamically initialise the rwlock with default attributes pthread_rwlock_init(&lock, NULL); pthread_create(&thread1, NULL, sum, (void*)1); pthread_create(&thread1, NULL, average, (void*)2); pthread_create(&thread1, NULL, max, (void*)3); pthread_create(&thread2, NULL, makerandom, NULL); pthread_join(thread1, NULL); pthread_join(thread2, NULL); return 0; }
This example shows all three forms of synchronization control available to POSIX threads:
- A mutex is used to ensure that only one thread is modifying the run flag at a time.
- A condition variable is used to notify flags that new values are in an array.
- A read/write lock is used to protect the array itself.