To avoid ambiguity when two or more threads access the same resource, mutual exclusion implements serializing access to the shared resources. When one thread is using a resource, no other thread is allowed to access the same resource. All of the other threads are blocked from accessing the same resource until the resource is free again.
A mutex is basically a lock that is associated with the shared resource. To read or modify the shared resource, a thread must first acquire the lock for that resource. Once a thread acquires a lock (or mutex) for that resource, it can go ahead with processing that resource. All of the other threads that wish to work on it will be compelled to wait until the resource is unlocked. When the thread finishes its processing on the shared resource, it unlocks the mutex, enabling the other waiting threads to acquire a mutex for that resource. Aside from mutex, a semaphore is also used in process synchronization.
A semaphore is a concept that is used to avoid two or more processes from accessing a common resource in a concurrent system. It is basically a variable that is manipulated to only allow one process to have access to a common resource and implement process synchronization. A semaphore uses the signaling mechanism, that is, it invokes wait and signal functions, respectively, to inform that the common resource has been acquired or released. A mutex, on the other hand, uses the locking mechanism—the process has to acquire the lock on the mutex object before working on the common resource.
Although mutex helps to manage shared resources among threads, there is a problem. An application of mutex in the wrong order may lead to a deadlock. A deadlock occurs in a situation when a thread that has lock X tries to acquire lock Y to complete its processing, while another thread that has lock Y tries to acquire lock X to finish its execution. In such a situation, a deadlock will occur, as both of the threads will keep waiting indefinitely for the other thread to release its lock. As no thread will be able to finish its execution, no thread will be able to free up its locks, either. One solution to avoid a deadlock is to let threads acquire locks in a specific order.
The following functions are used to create and manage threads:
- pthread_join: This function makes the thread wait for the completion of all its spawned threads. If it is not used, the thread will exit as soon as it completes its task, ignoring the states of its spawned threads. In other words, pthread_join blocks the calling thread until the thread specified in this function terminates.
- pthread_mutex_init: This function initializes the mutex object with the specified attributes. If NULL is used for the attributes, the default mutex attributes are used for initializing the mutex object. When the mutex is initialized, it is in an unlocked state.
- pthread_mutex_lock: This function locks the specified mutex object. If the mutex is already locked by some other thread, the calling thread will get suspended, that is, it will be asked to wait until the mutex gets unlocked. This function returns the mutex object in a locked state. The thread that locks the mutex becomes its owner and remains the owner until it unlocks the mutex.
- pthread_mutex_unlock: This function releases the specified mutex object. The thread that has invoked the pthread_mutex_lock function and is waiting for the mutex to get unlocked will become unblocked and acquire the mutex object, that is, the waiting thread will be able to access and lock the mutex object. If there are no threads waiting for the mutex, the mutex will remain in the unlocked state without any owner thread.
- pthread_mutex_destroy: This function destroys a mutex object and frees up the resources allocated to it. The mutex must be in an unlocked state before invoking this method.
That is enough theory. Now, let's start with some practical examples!