Mutex
A mutex (short for "mutual exclusion") is a synchronization primitive that allows us to protect access to data when we have (potentially) concurrent writes. It functions as a "lock" that safeguards access to shared resources.
A mutex is used to define a critical region, which is a section of the program where at most one thread can be at any given time. If a thread T1 attempts to enter a critical region when another thread T0 is already inside, T1 will block until T0 exits the critical region.
In the example above, we could use a mutex to define a critical region around the operation of incrementing a, making it impossible for the two threads' operations to interleave. The first thread that enters the critical region will exclusively increment a to 2, and the second thread cannot increment a until it is already 2.
A mutex has two primary operations: locking (lock) and unlocking (unlock). Through locking, a thread marks entry into the critical region, indicating that any other thread attempting to perform a locking operation will have to wait. Unlocking signifies the exit from the critical region, granting permission for another thread to enter the critical region.
In Pthreads, a typical sequence of using a mutex looks like this:
- Create and initialize a mutex variable.
- Multiple threads attempt to lock the mutex (i.e., enter the critical region).
- Only one of them succeeds and holds the mutex (i.e., is inside the critical region).
- The thread inside the critical region performs various operations on the protected data.
- The thread holding the mutex unlocks it, exiting the critical region.
- Another thread enters the critical region and repeats the process.
- Finally, the mutex variable is destroyed.
In Pthreads, a mutex is represented by a variable of type pthread_mutex_t
, and it is initialized using the following function:
int pthread_mutex_init(pthread_mutex_t *mutex, const pthread_mutexattr_t *attr);
The first parameter represents a reference to the mutex variable, and the second parameter specifies the attributes of the newly created mutex (if default behavior is desired, the attr
parameter can be left as NULL
).
To deallocate a mutex, the following function is used, which takes a pointer to the mutex to be destroyed as a parameter:
int pthread_mutex_destroy(pthread_mutex_t *mutex);
To lock a mutex, the following function is used, which takes the mutex as its parameter:
int pthread_mutex_lock(pthread_mutex_t *mutex);
The reverse operation, which specifies the exit from a critical region (i.e., unlocking the mutex), is performed using the following function:
int pthread_mutex_unlock(pthread_mutex_t *mutex);
All four mutex functions return 0 if executed successfully or an error code otherwise.
A graphical representation of how a mutex operates can be seen in the figure below, in a scenario where there are two threads (T0 and T1) and a critical region controlled by a mutex (the black box in the image). At time 1 (on the left side of the figure), T0 attempts to enter the critical region. Because, at that particular moment, no other thread holds the mutex (i.e., is inside the critical region), T0 enters the critical region (time 2). Later, when T1 arrives at the entrance of the critical region (tries to lock the mutex) at time 3, it gets blocked because the mutex is currently held by T0 (which is inside the critical region). Only when T0 exits the critical region (at time 4) can T1 become unblocked and continue its execution.
If we want to protect a section of our program using a mutex, then every thread that accesses that section must both lock and unlock the same mutex variable. Additionally, if a thread attempts to unlock a mutex it does not hold (has not previously locked), it will result in undefined behavior.