POSIX Threads provide multiple flows of execution within a process. The threads have their own stacks but share the global data and heap. So the global variables are visible to multiple threads. Also, the threads need to synchronize their actions so that they jointly realize the overall objectives of the process they belong to. The core problems of concurrent programming, mutual exclusion and synchronization are relevant for threads just like these problems are relevant for multi-process systems.
The global variables are visible to multiple threads. To illustrate the need for mutual exclusion, suppose, in a banking application, there is a variable named account_balance, with the value 7000. There are two threads, viz., check_clearance_thread and atm_withdrawal_thread. A customer tries to withdraw 5000 at an ATM. The atm_withdrawal_thread thread gets activated, the account_balance variable is checked and as it has 7000, the withdrawal processing starts. As luck would have it, a check for 4000 comes up for processing at the same time for the customer withdrawing the money via ATM. The check_clearance_thread thread gets activated, the account_balance is checked and found to have 7000, and the check clearance processing starts. By the time the two threads finish, the atm_withdrawal_thread has deducted 5000 from the account_balance and the check_clearance_thread has deducted another 4000 from the account_balance leaving a net -2000 in account_balance. So, there is a serious flaw in our logic. The catch is that the part of processing starting from reading the value of account_balance, processing and finally writing the new balance is a critical section of the code and only one thread should execute it at a time. If any other thread wants to access the account_balance, it should be blocked till the time the first thread finishes its processing and writing the final update of account_balance. The two threads must mutually exclude each other from the critical section. The problem of mutual exclusion is solved using a binary semaphore in the case of concurrent processes and threads. For Pthreads, we have a special semaphore for mutual exclusion known as a mutex object. If there are calls analogous to P (mutex) and V (mutex) at the start and end of the critical section of code, only one thread would execute the critical section at any time.
The simplest way to initialize a mutex is to define and initialize it as a global variable.
This can only be done for global variables. For automatic and dynamically allocated variables, it is necessary to initialize the mutex with the pthread_mutex_init call.
The basic calls for using mutex are the pthread_mutex_lock and pthread_mutex_unlock calls.
pthread_mutex_lock locks the mutex identified by the pointer passed as the argument. If the mutex is already locked, the call blocks till the time mutex becomes available for locking. pthread_mutex_lock provides the P operation for the mutex semaphore.
pthread_mutex_unlock unlocks the mutex identified by the pointer passed as the argument. pthread_mutex_unlock provides the V operation for the mutex semaphore.
Mutex's are binary semaphores that help threads in mutually excluding each other from executing critical sections of code concurrently. There is another class of synchronization problem where a counting semaphore value represents the number of instances of resource available and P (semaphore) represents acquiring an instance of resource and V (semaphore) represents releasing an instance. That way processes or threads can work easily with available number of instances of that resource concurrently. Pthreads provides condition variables that help in solving this problem in an easy way. Actually, we are interested in the condition, resource_is_available . If an instance of that resource is available, we can go ahead and take it. If no instance of that resource is available, we wait for a resource to become available.
To solve the problem of synchronized usage of a number of instances of a resource by multiple threads, we need a condition variable, a mutex and a predicate. A predicate could be something like, a resource instance is available . A condition variable is used for signalling the state of predicate. The mutex provides the mutual exclusion protection for data between multiple threads.
A thread releasing an instance of resource has code like this.
A thread acquiring an instance of resource has code similar to this.
The code for the first thread is quite straightforward. It first locks the mutex. It releases an instance of resource and unlocks the mutex. The code for the second thread is interesting. It locks the mutex. Then it does a cond_wait on conditional variable, cond using mutex. As soon as the conditional wait starts, the mutex is released. This ensures that the first thread can lock the mutex and do a conditional signal on cond. When cond_wait returns, the mutex is locked and is owned by Thread 2. Thread 2 can acquire the resource, update resource database and, then, unlock the mutex. cond_wait is put in a while loop because a signal might interrupt it, causing it to return without a resource instance becoming available.
The simplest way to initialize a conditional variable is to define and initialize it as a global variable.
This can only be done for global variables. For automatic and dynamically allocated variables, it is necessary to initialize with the pthread_cond_init call.
The basic calls for using conditional variables are pthread_cond_wait and pthread_cond_signal.
pthread_cond_wait blocks on the condition variable pointed by cond. The mutex is released at the start of block. Some other thread can lock the mutex, issue pthread_cond_signal and unlock the mutex. pthread_cond_wait, then, returns with the mutex locked and owned by the calling thread.
pthread_cond_signal unblocks a thread waiting on the condition variable pointed by cond.
The following example program illustrates the usage of Pthreads mutex and conditional variable calls. It solves the producer - consumer problem. There are ten producer threads that produce lines of text. The consumer thread prints these lines on the terminal.
// No more strings to print?
We can compile and run the above program as below.