CodingBison

Mutexes allow us to achieve synchronization, that is among a set of threads, who gets to access the critical data first. But, this is just one side of the Pthread story. For cases where the thread needs to wait for some common data, how does it know how long to wait? For such cases, threads need to communicate with each other.

Let us consider a simple case of two threads. Let us also say that the first thread produces a data and the second thread consumes that data. It is possible that when the second thread is trying to consume data, there is no data and so it must wait. Without explicit communication, the second thread would have to wait and check periodically if the resource has become available. Imagine kids sitting in the car on a road-trip and asking continuously, "Are we there yet?".

For such cases, the second thread should ideally wait till the first thread is done producing the resource. Once done, the first thread can communicate to the second thread to go ahead. Using an explicit communication for such use-cases is more efficient.

Pthreads achieve this inter-thread communication using conditional variables (or condvars for short). Condvars work hand in hand with mutexes. Pthreads provides a new variable type for condvars as well: pthread_cond_t. Thus, if we have to define a Pthread condvar variable, x, then we can do it as "pthread_cond_t x".

First of all, let us begin by putting together some of the common Pthread condvar APIs. Here they are:

 int pthread_cond_wait(pthread_cond_t *c, pthread_mutex_t *m);
 int pthread_cond_timedwait(pthread_cond_t *c, pthread_mutex_t *m, struct timespec *t);
 int pthread_cond_signal(pthread_cond_t *c);
 int pthread_cond_broadcast(pthread_cond_t *c);
 int pthread_cond_init(pthread_cond_t *c, pthread_condattr_t *attr);
 int pthread_cond_destroy(pthread_cod_t *c);

The first API in the above list, pthread_cond_wait() allows us to wait on a condvar; it takes both a condvar and a mutex as parameters. The caller must lock the mutex before calling pthread_cond_wait(), otherwise, the behavior is undefined! If the pthread_cond_wait() is successful, then it immediately unlocks the mutex and the then blocks the current thread. The idea behind unlocking the mutex is some other thread (hopefully, the thread for which we are waiting!) can acquire the mutex and continue its work.

Function pthread_cond_timedwait() is same as pthread_cond_wait(), except that it takes a time value as a parameter; if the time of wait exceeds the specified time value, then pthread_cond_timedwait() returns with an error of ETIMEDOUT. Threads blocked on pthread_cond_wait() and pthread_cond_timedwait() are often referred to as waiter threads.

The next API, pthread_cond_signal(), helps a thread signal the waiter threads and tell that it has provided the resource for which they are waiting. So now, a waiter thread can wake up and continue their task. As soon as a waiter thread receives this signal, pthread_cond_wait() unblocks and with unblocking, it immediately acquires the mutex. Once the thread is done with its task, it should call pthread_mutex_unlock() to release the mutex. Note that the Pthread signal is not the same as Linux/Unix signals (like SIGKILL, SIGTERM, etc) -- these two types are unrelated.

Instead of pthread_cond_signal(), a thread can also call pthread_cond_broadcast(), if there are more than one waiter threads. Using pthread_cond_broadcast() is more efficient since it informs all the waiter threads in one shot. If multiple threads are waiting, then the thread that gets the lock (remember, pthread_cond_wait() returns and implicitly acquires the lock) depends upon the scheduling policy. In essence, it is same as multiple threads vying to lock a single mutex. Those who fail to lock the mutex must continue to play the waiting game!

Needless to say, if there are no waiter threads, then pthread_cond_signal() and pthread_cond_broadcast() do not do anything.

Like the case of mutexes, the last two APIs allow us to initialize and destroy a condvar dynamically. We can use pthread_cond_init() to initialize a condvar variable. Once done, we can use pthread_cond_destroy() to release resources attached with it.

It is also possible to define (and initialize) a condvar variable statically (meaning that the scope is only the current file) using the PTHREAD_COND_INITIALIZER macro; this macro contains a default values for various attributes of a condvar. Thus, if want to define a condvar statically, then we can do that as: "pthread_cond_t x = PTHREAD_COND_INITIALIZER;". For statically defined condvars, there is no need to call pthread_cond_destroy().

With that, let us provide an example, where two threads, threadPaintingsIn and threadPaintingsOut, use a condvar to communicate with each other. The thread threadPaintingsIn mimics buying of paintings from artists by incrementing a global variable, totalPaintings. The thread threadPaintingsOut mimics selling of paintings to end-users by decrementing the same global variable, totalPaintings.

If there are no paintings, then threadPaintingsOut uses pthread_cond_wait() to simply wait. The threadPaintingsIn sleeps for a random interval and upon waking up, adds a new painting. After it adds a new painting, it uses pthread_cond_signal() to signal the waiter thread.

 #include <stdio.h>
 #include <pthread.h>
 #include <time.h>
 #include <stdbool.h>

 #define TOTAL_TRANSACTIONS 5
 #define MAX_SLEEP          5

 static pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
 static pthread_cond_t condvar = PTHREAD_COND_INITIALIZER;

 static int totalPaintings = 0;
 static bool allTransactionsDone = false;
 static bool thread_waiting = false;

 /* Sell the paintings from the inventory */
 void *paintingsOut (void *arg) {
     int random_time;

     while ((allTransactionsDone == false) || totalPaintings) {  
         pthread_mutex_lock(&mutex);
         if (!totalPaintings) {
             thread_waiting = true;
             printf("\t\t[%s]No more paintings left. Let us wait..\n", __FUNCTION__);
             pthread_cond_wait(&condvar, &mutex);
             thread_waiting = false;
         }

         totalPaintings--;
         printf("\t\t[%s]Sold one painting. Total paintings left: %d\n", __FUNCTION__, totalPaintings);
         pthread_mutex_unlock(&mutex);
     }
 }

 /* Buy painting from artists and add to the Inventory */
 void *paintingsIn (void *arg) {
     int i, random_time;

     srand(time(NULL));
     for (i = 0; i < TOTAL_TRANSACTIONS; i++) {
         random_time = rand() % MAX_SLEEP;
         printf("\t[%s]Sleep for %d seconds. \n", __FUNCTION__, random_time);
         sleep(random_time);

         pthread_mutex_lock(&mutex);
         totalPaintings++;
         printf("\t[%s]Added one painting. Total paintings are: %d\n", 
                     __FUNCTION__, totalPaintings);
         pthread_mutex_unlock(&mutex);
         if (thread_waiting) {
             pthread_cond_signal(&condvar);
         }
     }
     allTransactionsDone = true;
 }

 int main () {
     pthread_t threadPaintingsIn, threadPaintingsOut;
     int status;

     status = pthread_create(&threadPaintingsIn, NULL, paintingsIn, NULL);
     if (status != 0) {
         fprintf(stderr, "pthread_create() failed [status: %d]\n", status);
         return 0;
     }

     status = pthread_create(&threadPaintingsOut, NULL, paintingsOut, NULL);
     if (status != 0) {
         fprintf(stderr, "pthread_create() failed [status: %d]\n", status);
         return 0;
     }

     printf("Waiting for the threadPaintingsIn..\n");
     status = pthread_join(threadPaintingsIn, NULL);
     if (status != 0) {
         fprintf(stderr, "pthread_join() failed for threadPaintingsIn [%d]\n", status);
     }

     printf("Waiting for the threadPaintingsOut..\n");
     status = pthread_join(threadPaintingsOut, NULL);
     if (status != 0) {
         fprintf(stderr, "pthread_join() failed for threadPaintingsOut [%d]\n", status);
     }
     return 0;
 }

Some additional notes on the above program.

For simplicity sake, the threadPaintingsIn does not run forever. It runs only for the first TOTAL_TRANSACTIONS transactions and then returns. Before returning, the thread sets a global boolean variable, allTransactionsDone to true, indicating that no more paintings would be added to the inventory.

The threadPaintingsOut stops when allTrasactionsDone becomes true and when it is done selling all the remaining paintings. On the other hand, if there are no paintings and allTrasactionsDone is still false, then it uses pthread_cond_wait() to wait till threadPaintingsIn signals it to wake up.

Lastly, we add varying levels of indentation (the tabs "\t" in the printf() statements) to improve readability of the output: the events of the main thread have no tabs, the events when paintings arrive in the inventory have a single tab, and the events when paintings are sold have two tabs.

To run the above example, we compile it with "-pthread" option that adds support for Pthreads library. The output shows that with condvar, threadPaintingsOut behaves patiently -- if there is no painting, then it waits on the condvar and tries only when it receives a signal from threadPaintingsIn.

 [user@codingbison]$ gcc condvar_two_threads.c -pthread -o condvar-two-threads
 [user@codingbison]$ 
 [user@codingbison]$ ./condvar-two-threads 
 Waiting for the threadPaintingsIn..
 		[paintingsOut]No more paintings left. Let us wait..
 	[paintingsIn]Sleep for 0 seconds. 
 	[paintingsIn]Added one painting. Total paintings are: 1
 	[paintingsIn]Sleep for 2 seconds. 
 		[paintingsOut]Sold one painting. Total paintings left: 0
 		[paintingsOut]No more paintings left. Let us wait..
 	[paintingsIn]Added one painting. Total paintings are: 1
 	[paintingsIn]Sleep for 2 seconds. 
 		[paintingsOut]Sold one painting. Total paintings left: 0
 		[paintingsOut]No more paintings left. Let us wait..
 	[paintingsIn]Added one painting. Total paintings are: 1
 	[paintingsIn]Sleep for 1 seconds. 
 		[paintingsOut]Sold one painting. Total paintings left: 0
 		[paintingsOut]No more paintings left. Let us wait..
 	[paintingsIn]Added one painting. Total paintings are: 1
 	[paintingsIn]Sleep for 1 seconds. 
 		[paintingsOut]Sold one painting. Total paintings left: 0
 		[paintingsOut]No more paintings left. Let us wait..
 	[paintingsIn]Added one painting. Total paintings are: 1
 Waiting for the threadPaintingsOut..
 		[paintingsOut]Sold one painting. Total paintings left: 0
 [user@codingbison]$ 




comments powered by Disqus