Synchronization between multiple threads is necessary to ensure the stability and correctness of a program. Here are the key reasons why synchronization is necessary:
- Avoid race conditions: Race conditions occur when two or more threads access shared data simultaneously and at least one thread modifies the data. This can lead to inconsistent or unexpected results.
- Ensuring data consistency: Threads can see stale or partial data without synchronization. For instance, if one thread is writing to a shared variable while another thread reads it, the reader might get an inconsistent view of the data.
- Coordinating thread execution: Sometimes, threads need to coordinate their actions.
- Avoiding deadlocks and starvation: Deadlocks occur when two or more threads are waiting indefinitely for each other to release resources. Proper synchronization can help design systems that avoid deadlocks by careful ordering of lock acquisition.
- Managing shared resources: In multithreaded programs, resources like files, databases, or network connections might be shared among threads. Synchronization ensures these resources are accessed safely and efficiently, preventing conflicts and ensuring consistency.
Thread library provides the some of the following synchronization mechanisms:
- Mutexes: Mutex is mutual exclusion lock used to protect data from concurrent accesses. Only one thread can hold a mutex at a time. There are 3 types of mutexes: normal, errorcheck, and recursive.
Below is the example code to use mutexes:
- Condition variables: Condition variables allow threads to wait until some event or condition has occurred. Condition variables facilitate communication between threads by enabling them to signal each other when conditions change. Condition variables in C are synchronization primitives that allow threads to wait until a certain condition occurs. They are used with mutexes to coordinate the order of execution of threads based on certain conditions.
How condition variables work:
- Waiting: A thread can wait on a condition variable. When waiting, the thread releases the associated mutex and enters a sleep state.
- Signaling: Another thread can signal the condition variable to wake up one or more waiting threads. The signaled threads then re-acquire the mutex and continue execution.
Typical Usage Pattern
- A thread locks a mutex.
- It checks a condition.
- If the condition is not met, it waits on a condition variable, releasing the mutex.
- Another thread signals the condition variable when the condition is met.
- The waiting thread wakes up, re-acquires the mutex, and re-checks the condition.
Example:
Declaring the variable along with mutex and condition:
In the following example, thread waits for the condition until 8:00 January 1, 2001 local time:
A condition can be signaled by calling either the pthread_cond_signal or the pthread_cond_broadcast subroutine. The pthread_cond_wait and the pthread_cond_broadcast subroutines must not be used within a signal handler.
It is recommended that a condition wait be enclosed in a “while loop” that checks the predicate. Basic implementation of a condition wait is shown in the following code fragment:
When the pthread_cond_timedwait subroutine returns with the timeout error, the predicate may be true, due to another unavoidable race between the expiration of the timeout and the predicate state change.
- rw locks:Read-write locks (also known as shared-exclusive locks) are synchronization primitives that allow multiple threads to read from a shared resource concurrently while giving exclusive access to a single thread for writing. They help optimize scenarios where reads are more frequent than writes, allowing better concurrency comparing with a simple mutex.
The pthread_rwlockattr_init subroutine initializes a read-write lock attributes object (attr). The pthread_rwlock_init subroutine initializes the read/write lock referenced by the rwlock object with the attributes referenced by the attr object. The pthread_rwlock_destroy subroutine destroys the read/write lock object referenced by the rwlock object and releases any resources used by the lock. The pthread_rwlock_rdlock subroutine applies a read lock. The pthread_rwlock_wrlock subroutine applies a write lock.
example:
Joining threads: Joining a thread means waiting for it to terminate, which can be seen as a specific usage of condition variables. The pthread_join subroutine blocks the calling thread until the specified thread terminates. The target thread must not be detached.The following table indicates the possible cases when a thread calls the pthread_join subroutine, depending on the state and the detachstate attribute of the target thread:
Several threads can join the same target thread, if the target is not detached. The success of this operation depends on the order of the calls to the pthread_join subroutine and the moment when the target thread terminates.
- Any call to the pthread_join subroutine occurring before the target thread’s termination blocks the calling thread.
- When the target thread terminates, all blocked threads are awoken, and the target thread is automatically detached.
- Any call to the pthread_join subroutine occurring after the target thread’s termination will fail, because the thread is detached by the previous join.
- If no thread called the pthread_join subroutine before the target thread’s termination, the first call to the pthread_join subroutine will return immediately, indicating a successful completion, and any further call will fail.
Atomic variables: Atomic variables provide atomic operations that are indivisible. Common operations include load, store, exchange, compare-and-swap (CAS), fetch-and-add, and fetch-and-subtract. These operations ensure that the variable is updated consistently even when accessed by multiple threads simultaneously.
C11 introduced atomic operations in <stdatomic.h>:
Semaphores: Semaphores are synchronization primitives that are widely used in multithreaded programming to control access to shared resources. They can be used to solve various concurrency problems, such as ensuring mutual exclusion, coordinating the order of execution, and limiting the number of threads accessing a resource. Samaphores of two types : counting semaphores and binary semaphores.
A semaphore has the sema_t data type. It must be initialized by the sema_init routine and destroyed with the sema_destroy routine. The semaphore wait and semaphore post operations are performed by using the sema_p and sema_v routines.
ex:
Python example: