In-depth Understanding of pthread_cond_wait and pthread_cond_signal
===============================Explanation from man pthread_cond_wait==========================
In a LINUX environment, multithreaded programming will inevitably encounter situations requiring condition variables, which necessitates the use of the pthread_cond_wait() function. However, the execution process of this function can be difficult to understand.
The workflow of pthread_cond_wait() is as follows (using the EXAMPLE from the MAN page):
Consider two shared variables x and y, protected by the mutex mut, and a condition vari-
able cond that is to be signaled whenever x becomes greater than y.
int x,y;
pthread_mutex_t mut = PTHREAD_MUTEX_INITIALIZER;
pthread_cond_t cond = PTHREAD_COND_INITIALIZER;
Waiting until x is greater than y is performed as follows:
pthread_mutex_lock(&mut);
while (x <= y) {
pthread_cond_wait(&cond, &mut);
}
/* operate on x and y */
pthread_mutex_unlock(&mut);
Modifications on x and y that may cause x to become greater than y should signal the con- dition if needed:
pthread_mutex_lock(&mut);
/* modify x and y */
if (x > y) pthread_cond_broadcast(&cond);
pthread_mutex_unlock(&mut);
This example means that two threads will modify the values of X and Y. The first thread, when X <= Y, will suspend until X > Y to continue execution (the second thread might modify X, Y, and wake up the first thread when X > Y). That is, first, a regular mutex mut and a condition variable cond are initialized. Then, the following function bodies are executed in two separate threads:
pthread_mutex_lock(&mut);
while (x <= y) {
pthread_cond_wait(&cond, &mut);
}
/* operate on x and y */
pthread_mutex_unlock(&mut);
and:
pthread_mutex_lock(&mut);
/* modify x and y */
if (x > y) pthread_cond_signal(&cond);
pthread_mutex_unlock(&mut);
In fact, the function's execution process is very simple. When the first thread executes pthread_cond_wait(&cond,&mut), if X <= Y, this function unlocks the mut mutex, then locks the cond condition variable, and the first thread suspends (not occupying any CPU cycles).
Meanwhile, the second thread, which was blocked because mut was locked by the first thread, can now acquire the mut lock because mut has been released. It then modifies the values of X and Y. After modification, an IF statement checks if X > Y. If it is, the pthread_cond_signal() function wakes up the first thread, and in the next statement, releases the mut mutex. Then the first thread resumes execution from pthread_cond_wait(), first by locking mut again. If the lock is successful, it then checks the condition (the reason for using WHILE, i.e., re-checking after being woken up, will be analyzed later). If the condition is met, it is woken up to process, and finally releases the mut mutex.
As for why the condition needs to be re-checked after being woken up (i.e., why a while loop is used to check the condition), it's because of the potential for "spurious wakeups" or "thundering herd problem". Some people think that since it's woken up, the condition must be met, but this is not necessarily true. If multiple threads are waiting for this condition, but only one thread can process at a time, then the condition must be re-checked to ensure that only one thread enters the critical section for processing. Regarding this, here's a quote:
Quoting POSIX RATIONALE:
Condition Wait Semantics
It is important to note that when pthread_cond_wait() and pthread_cond_timedwait() return without error, the associated predicate may still be false. Similarly, when pthread_cond_timedwait() returns with the timeout error, the associated predicate may be true due to an unavoidable race between the expiration of the timeout and the predicate state change.
The application needs to recheck the predicate on any return because it cannot be sure there is another thread waiting on the thread to handle the signal, and if there is not then the signal is lost. The burden is on the application to check the predicate.
Some implementations, particularly on a multi-processor, may sometimes cause multiple threads to wake up when the condition variable is signaled simultaneously on different processors.
In general, whenever a condition wait returns, the thread has to re-evaluate the predicate associated with the condition wait to determine whether it can safely proceed, should wait again, or should declare a timeout. A return from the wait does not imply that the associated predicate is either true or false.
It is thus recommended that a condition wait be enclosed in the equivalent of a "while loop" that checks the predicate.
From the above, we can see:
pthread_cond_signalon a multiprocessor system might wake up multiple threads simultaneously. If you only want one thread to handle a specific task, other woken threads need to continue waiting. This is where thewhileloop comes into play. The specification requirespthread_cond_signalto wake up at least one thread waiting onpthread_cond_wait. In fact, some implementations, for simplicity, might wake up multiple threads even on a single processor.- In certain applications, such as thread pools,
pthread_cond_broadcastwakes up all threads, but we usually only need a portion of them to execute tasks, so the other threads need to continue waiting. Therefore, it is strongly recommended to use awhileloop here.
In simple terms, pthread_cond_signal() can also wake up multiple threads, and if you only allow one thread to access at a time, you must use a while loop for condition checking to ensure that only one thread is processing within the critical section.
pthread_cond_wait() is used to block the current thread, waiting for another thread to wake it up using pthread_cond_signal() or pthread_cond_broadcast. pthread_cond_wait() must be used in conjunction with pthread_mutex. The pthread_cond_wait() function automatically releases the mutex upon entering the wait state. When another thread wakes up this thread via pthread_cond_signal() or pthread_cond_broadcast, causing pthread_cond_wait() to return, the thread automatically reacquires the mutex.
The purpose of the pthread_cond_signal function is to send a signal to another thread that is in a blocked waiting state, causing it to exit the blocked state and continue execution. If no thread is in a blocked waiting state, pthread_cond_signal will still return successfully.
Using pthread_cond_signal generally does not cause a "thundering herd problem"; it signals at most one thread. If multiple threads are blocked and waiting for this condition variable, the thread that receives the signal and continues execution is determined by their priorities. If priorities are equal, it's determined by the length of time they've been waiting. Regardless, a single pthread_cond_signal call signals at most once.
However, pthread_cond_signal on a multiprocessor system might wake up multiple threads simultaneously. If you only want one thread to handle a specific task, other woken threads need to continue waiting.
==============================Another Excellent Article===========================
==============================Efficiency Issues============================
The purpose of the pthread_cond_signal function is to send a signal to another thread that is in a blocked waiting state, causing it to exit the blocked state and continue execution. If no thread is in a blocked waiting state, pthread_cond_signal will still return successfully.
However, using pthread_cond_signal does not cause a "thundering herd problem"; it signals at most one thread. If multiple threads are blocked and waiting for this condition variable, the thread that receives the signal and continues execution is determined by their priorities. If priorities are equal, it's determined by the length of time they've been waiting. Regardless, a single pthread_cond_signal call signals at most once.
Additionally, the role of a mutex is generally to provide exclusive access to a resource, often to ensure that an operation is atomic and cannot be interrupted.
Usage:
pthread_cond_wait must be placed between pthread_mutex_lock and pthread_mutex_unlock because it needs to decide whether to wait based on the shared variable's state, and to avoid waiting indefinitely, it must be within the lock/unlock pair.
Changes to the shared variable's state must adhere to the lock/unlock rules.
pthread_cond_signal can be placed either between pthread_mutex_lock and pthread_mutex_unlock, or after pthread_mutex_unlock. However, each approach has its drawbacks.
Between:
pthread_mutex_lock xxxxxxx pthread_cond_signal pthread_mutex_unlock
Drawback: In some thread implementations, this can cause the waiting thread to wake up from the kernel (due to cond_signal) and then immediately return to kernel space (because cond_wait performs an atomic lock upon returning), leading to a performance overhead due to the context switch.
In code reviews, I often find that many people like to call pthread_cond_signal or pthread_cond_broadcast between pthread_mutex_lock() and pthread_mutex_unlock(). Logically, this usage is perfectly correct. However, in a multithreaded environment, this approach might be inefficient. The POSIX.1 standard states that pthread_cond_signal and pthread_cond_broadcast do not require the calling thread to be the mutex owner, meaning they can be called outside the lock/unlock region. If we don't care about the calling behavior, then please call them outside the lock region. Here's an example:
Assume we have Thread 1 and Thread 2. Both want to acquire the mutex, process shared data, and then release the mutex. Consider this sequence:
- Thread 1 acquires the mutex. While it's processing data, Thread 2 also wants to acquire the mutex but is blocked because Thread 1 holds it. Thread 2 goes to sleep, waiting for the mutex to be released.
- After Thread 1 finishes data processing, it calls
pthread_cond_signal()to wake up a thread in the waiting queue, which in this example is Thread 2. Before Thread 1 callspthread_mutex_unlock(), due to system scheduling, Thread 2 gets CPU time and wants to start processing data. However, before it can start, the mutex must be acquired. Unfortunately, Thread 1 is still using the mutex, so Thread 2 is forced to go back to sleep. - Only after Thread 1 executes
pthread_mutex_unlock()can Thread 2 be woken up again. From this, it's clear that the efficiency is relatively low. If this situation occurs frequently in a multithreaded environment, it can be quite problematic.
However, in LinuxThreads or NPTL, this problem does not exist. In Linux threads, there are two queues: the cond_wait queue and the mutex_lock queue. cond_signal merely moves a thread from the cond_wait queue to the mutex_lock queue without returning to user space, thus incurring no performance loss.
Therefore, this pattern is recommended in Linux.
After:
pthread_mutex_lock xxxxxxx pthread_mutex_unlock pthread_cond_signal
Advantage: The potential performance overhead mentioned earlier does not occur, as the lock is released before the signal.
Drawback: If a lower-priority thread is waiting on the mutex between the unlock and signal calls, that lower-priority thread might preempt the higher-priority thread (the one waiting on cond_wait), which would not happen in the "between" pattern.
Therefore, in Linux, it's best to place pthread_cond_signal between the lock and unlock, but from a programming rule perspective, both other methods are also valid.