With multiple threads, it is possible that one or more of them may end up accessing a common data (aka critical data) in the memory. If that happens, then we need to synchronize threads. Informally, synchronization means deciding which thread gets to access critical data first. All other threads simply need to play the waiting game; more specifically, these threads sit in the blocked state.
Pthreads achieve synchronization using mutex (short for mutual exclusion). Pthreads provides a new variable type for mutexes: pthread_mutex_t. Thus, if we have to define a Pthread mutex variable, x, then we can do it as "pthread_mutex_t x".
The easiest way to understand a mutex is to understand it as a lock such that only one thread can lock (or acquire) it at a given time. The thread that acquires the lock, gets to access the common data. In the meantime, all other threads simply wait for their turn. Once the thread with the lock is done accessing data, it can unlock the mutex. With that, one of the waiting threads will acquire the lock and access the data. And, thus the cycle of wait for mutex, lock mutex, access critical data, and unlock mutex continues. It is common to use the terms "lock a mutex" and "acquire a mutex" interchangeably.
First off, let us provide signatures of some of the common mutex APIs.
int pthread_mutex_lock(pthread_mutex_t *m); int pthread_mutex_unlock(pthread_mutex_t *m); int pthread_mutex_trylock(pthread_mutex_t *m); int pthread_mutex_init(pthread_mutex_t *m, pthread_mutexattr_t *attr); int pthread_mutex_destroy(pthread_mutex_t *m);
The first function in the above set, pthread_mutex_lock() locks a mutex. If the mutex is already locked, then the thread will wait for the mutex to becomes unlock. More specifically, the thread moves from running state to a blocked state and continues to remain blocked until the mutex is unlocked.
The next function, pthread_mutex_unlock() allows a thread to unlock a previously acquired mutex. Mutex is thread-specific and so, it can be unlocked only by the thread that holds the mutex; this thread is also referred to as the owner thread. The bad news is that if the owner thread were to terminate without unlocking the mutex, then the mutex would live in a locked state, sadly ever after! So, it is worth paying attention to threads when they terminate or exit and check if they are holding any mutex.
Since pthread_mutex_lock() would potentially block the current thread, sometimes a thread can use a handy shortcut to check if the mutex is locked or not. The thread can use pthread_mutex_trylock() to lock the mutex if it is available, else, this function returns immediately with a status of EBUSY. In the case of mutex being locked, the thread can go on and do something else and can retry later.
The last two APIs allow us to initialize and destroy a mutex variable dynamically. We can use pthread_mutex_init() to initialize a mutex dynamically. Once done, we should call pthread_mutex_destroy() to release resources attached with the mutex.
It is also possible for us to define (and initialize) a mutex variable statically (meaning that the scope is only the current file) using the PTHREAD_MUTEX_INITIALIZER macro; this macro contains a default values for various attributes of a mutex. Thus, if want to define a mutex statically, then we can do that as: "pthread_mutex_t x = PTHREAD_MUTEX_INITIALIZER;". For statically defined mutexes, there is no need to call pthread_mutex_destroy().
Before we go any further, let us see an example that uses Pthread mutex to synchronize two threads. The example mimics a paintings gallery, where paintings by various artists are continuously bought and then sold to art connoisseurs. Here it is:
#include <stdio.h> #include <pthread.h> #include <time.h> #include <stdbool.h> #define TOTAL_TRANSACTIONS 5 #define MAX_SLEEP_SECONDS_PAINTINGS_IN 10 #define MAX_SLEEP_SECONDS_PAINTINGS_OUT 5 static pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER; static int totalPaintings = 0; static bool allTransactionsDone = false; /* Sell the paintings from the inventory */ void *paintingsOut (void *arg) { int random_time; srand(time(NULL)); while ((allTransactionsDone == false) || totalPaintings) { random_time = rand() % MAX_SLEEP_SECONDS_PAINTINGS_OUT; printf("\t\t[%s]Sleep for %d seconds.\n", __FUNCTION__, random_time); sleep(random_time); pthread_mutex_lock(&mutex); if (!totalPaintings) { printf("\t\t[%s]No more paintings left. Let us retry.\n", __FUNCTION__); pthread_mutex_unlock(&mutex); continue; } 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_SECONDS_PAINTINGS_IN; 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); } 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; }
Let us understand various pieces of the above program.
The main thread defines two threads: threadPaintingsIn and threadPaintingsOut. 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. Both threads sleep for a while and then use a common mutex to protect this variable.
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. By keeping TOTAL_TRANSACTIONS small (equal to 5), we were able to get just the right amount of transactions to demonstrate the concept of mutex -- not too less, not too more!
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 the thread unlocks the mutex, sleeps for some time, and then retries.
The program intentionally keeps different values of the maximum time for sleep for the two threads. For threadPaintingsIn, the max value is MAX_SLEEP_SECONDS_PAINTINGS_IN seconds, where as for threadPaintingsOut, the max value is MAX_SLEEP_SECONDS_PAINTINGS_OUT seconds. By keeping different values, we create a case where the threadPaintingsOut wakes up more often and checks if there is anything in the inventory.
Lastly, if you are feeling uncomfortable with the varying levels of indentation (the tabs "\t" in the printf() statements), then, well it is also intentional! We have kept it 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 to add support for Pthreads library. Here is the output:
[user@codingbison]$ gcc mutex_two_threads.c -pthread -o mutex-two-threads [user@codingbison]$ [user@codingbison]$ ./mutex-two-threads Waiting for the threadPaintingsIn.. [paintingsIn]Sleep for 2 seconds. [paintingsOut]Sleep for 2 seconds. [paintingsIn]Added one painting. Total paintings are: 1 [paintingsIn]Sleep for 9 seconds. [paintingsOut]Sold one painting. Total paintings left: 0 [paintingsOut]Sleep for 3 seconds. [paintingsOut]No more paintings left. Let us retry. [paintingsOut]Sleep for 0 seconds. [paintingsOut]No more paintings left. Let us retry. [paintingsOut]Sleep for 2 seconds. [paintingsOut]No more paintings left. Let us retry. [paintingsOut]Sleep for 3 seconds. [paintingsOut]No more paintings left. Let us retry. [paintingsOut]Sleep for 3 seconds. [paintingsIn]Added one painting. Total paintings are: 1 [paintingsIn]Sleep for 5 seconds. [paintingsOut]Sold one painting. Total paintings left: 0 [paintingsOut]Sleep for 4 seconds. [paintingsIn]Added one painting. Total paintings are: 1 [paintingsIn]Sleep for 8 seconds. [paintingsOut]Sold one painting. Total paintings left: 0 [paintingsOut]Sleep for 2 seconds. [paintingsOut]No more paintings left. Let us retry. [paintingsOut]Sleep for 2 seconds. [paintingsOut]No more paintings left. Let us retry. [paintingsOut]Sleep for 1 seconds. [paintingsOut]No more paintings left. Let us retry. [paintingsOut]Sleep for 1 seconds. [paintingsOut]No more paintings left. Let us retry. [paintingsOut]Sleep for 3 seconds. [paintingsIn]Added one painting. Total paintings are: 1 [paintingsIn]Sleep for 0 seconds. [paintingsIn]Added one painting. Total paintings are: 2 Waiting for the threadPaintingsOut.. [paintingsOut]Sold one painting. Total paintings left: 1 [paintingsOut]Sleep for 1 seconds. [paintingsOut]Sold one painting. Total paintings left: 0
It is easy to see that both threads take turns to do their work and do not overwrite the common variable. In other words, they are synchronized. The threadPaintingsOut does (intentionally) wake up more often and if threadPaintingsIn did not add any paintings, then threadPaintingsOut unlocks the mutex and then later.
The output shows that this may not be optimal because threadPaintingsOut has to retry multiple times before there is some painting that needs to be sold. It would have been more efficient, if threadPaintingsOut were to wait till threadPaintingsIn adds a new painting. But, how does threadPaintingsOut know when threadPaintingsIn has added a new painting? One common way for the threadPaintingsIn to tell threadPaintingsOut that it has added a painting is to use Pthread condvars.