我正在阅读pthread.h;与条件变量相关的函数(如pthread_cond_wait(3))需要一个互斥量作为参数。为什么?据我所知,我要创建一个互斥量只是为了用作那个参数?互斥锁应该做什么?
这只是条件变量(或最初)实现的方式。
互斥锁用于保护条件变量本身。这就是为什么你在等待之前要把它锁上。
等待将“原子地”解锁互斥锁,允许其他人访问条件变量(用于发送信号)。然后,当条件变量被发送信号或广播到时,等待列表上的一个或多个线程将被唤醒,互斥锁将神奇地再次为该线程锁定。
您通常会看到以下使用条件变量的操作,说明它们是如何工作的。下面的例子是一个工作线程,它通过信号将工作分配给一个条件变量。
thread:
initialise.
lock mutex.
while thread not told to stop working:
wait on condvar using mutex.
if work is available to be done:
do the work.
unlock mutex.
clean up.
exit thread.
只要等待返回时有一些可用的工作,就在这个循环中完成。当线程被标记为停止工作时(通常是由另一个线程设置退出条件,然后启动条件变量来唤醒该线程),循环将退出,互斥锁将被解锁,该线程将退出。
上面的代码是一个单使用者模型,因为在工作完成时互斥锁保持锁定状态。对于多消费者的变化,您可以使用,作为一个例子:
thread:
initialise.
lock mutex.
while thread not told to stop working:
wait on condvar using mutex.
if work is available to be done:
copy work to thread local storage.
unlock mutex.
do the work.
lock mutex.
unlock mutex.
clean up.
exit thread.
它允许其他消费者在它做功的时候接收功。
条件变量减轻了轮询某些条件的负担,而不是允许另一个线程在需要发生某些事情时通知您。另一个线程可以告诉该线程工作可用,如下所示:
lock mutex.
flag work as available.
signal condition variable.
unlock mutex.
大多数通常被错误地称为虚假唤醒的情况通常都是因为在pthread_cond_wait调用(广播)中已经发出了多个线程的信号,其中一个线程会返回互斥锁,完成工作,然后重新等待。
然后,当没有工作要做的时候,第二条发出信号的线就可以出来了。所以你必须有一个额外的变量来指示应该做的工作(这是由condvar/互斥锁对固有的互斥锁保护的-其他线程需要在改变互斥锁之前锁定它)。
从技术上讲,线程从条件等待中返回而不被另一个进程踢出是可能的(这是一个真正的虚假唤醒),但是,在我多年的pthread工作中,无论是在代码的开发/服务还是作为它们的用户,我从来没有收到过这样的消息。也许这只是因为惠普有一个不错的实现:-)
在任何情况下,处理错误情况的相同代码也处理了真正的虚假唤醒,因为不会为它们设置工作可用标志。
当你调用pthread_cond_wait时,互斥锁应该是锁定的;当你调用它时,它会自动地解锁互斥锁,然后在条件上阻塞。一旦条件发出信号,它就会自动锁定它并返回。
这允许在需要的情况下实现可预测的调度,因为发送信号的线程可以等到互斥锁释放后再进行处理,然后发出条件信号。
一个条件变量是非常有限的,如果你只能信号一个条件,通常你需要处理一些数据,相关的条件被信号。信号/唤醒必须在不引入竞态条件的情况下实现,或者过于复杂
出于技术上的原因,pthread也会给你一个虚假的唤醒。这意味着您需要检查一个谓词,这样您就可以确保该条件确实发出了信号——并将其与虚假唤醒区分开来。检查这样一个条件是否等待它需要被保护——所以一个条件变量需要一种方法来原子地等待/唤醒,同时锁定/解锁一个保护该条件的互斥锁。
考虑一个简单的例子,在这个例子中,您被通知生成了一些数据。也许另一个线程创建了一些你想要的数据,并设置了指向该数据的指针。
想象一个生产者线程通过'some_data'将一些数据传递给另一个消费者线程 指针。
while(1) {
pthread_cond_wait(&cond); //imagine cond_wait did not have a mutex
char *data = some_data;
some_data = NULL;
handle(data);
}
你自然会得到很多竞态条件,如果另一个线程在你被唤醒后执行some_data = new_data,但在你执行data = some_data之前执行
你也不能创建自己的互斥锁来保护这种情况
while(1) {
pthread_cond_wait(&cond); //imagine cond_wait did not have a mutex
pthread_mutex_lock(&mutex);
char *data = some_data;
some_data = NULL;
pthread_mutex_unlock(&mutex);
handle(data);
}
将不起作用,在唤醒和抓取互斥量之间仍然有可能出现竞争条件。将互斥锁放在pthread_cond_wait之前并不能像现在这样帮助您 在等待时持有互斥量——也就是说,生产者将永远无法获取互斥量。 (注意,在这种情况下,您可以创建第二个条件变量来通知生产者您已经完成了some_data的处理——尽管这将变得复杂,特别是如果您想要许多生产者/消费者。)
因此,在等待/从条件中醒来时,需要一种方法来原子地释放/获取互斥量。这就是pthread条件变量所做的,下面是你要做的:
while(1) {
pthread_mutex_lock(&mutex);
while(some_data == NULL) { // predicate to acccount for spurious wakeups,would also
// make it robust if there were several consumers
pthread_cond_wait(&cond,&mutex); //atomically lock/unlock mutex
}
char *data = some_data;
some_data = NULL;
pthread_mutex_unlock(&mutex);
handle(data);
}
(生产者自然需要采取同样的预防措施,总是用相同的互斥量保护'some_data',并确保它不会覆盖some_data,如果some_data当前是!= NULL)
POSIX条件变量是无状态的。所以维护国家是你的责任。由于等待的线程和通知其他线程停止等待的线程都将访问该状态,因此必须使用互斥锁来保护它。如果您认为可以在没有互斥的情况下使用条件变量,那么您没有理解条件变量是无状态的。
条件变量是围绕条件构建的。等待条件变量的线程是在等待某种条件。发出条件变量信号的线程会改变该条件。例如,一个线程可能正在等待一些数据到达。其他线程可能会注意到数据已经到达。“数据已到”是条件。
下面是条件变量的经典用法,简化后:
while(1)
{
pthread_mutex_lock(&work_mutex);
while (work_queue_empty()) // wait for work
pthread_cond_wait(&work_cv, &work_mutex);
work = get_work_from_queue(); // get work
pthread_mutex_unlock(&work_mutex);
do_work(work); // do that work
}
查看线程是如何等待工作的。工作由互斥锁保护。等待释放互斥量,以便另一个线程可以给这个线程一些工作。它是如何发出信号的:
void AssignWork(WorkItem work)
{
pthread_mutex_lock(&work_mutex);
add_work_to_queue(work); // put work item on queue
pthread_cond_signal(&work_cv); // wake worker thread
pthread_mutex_unlock(&work_mutex);
}
注意,您需要互斥来保护工作队列。注意,条件变量本身并不知道是否有功。也就是说,条件变量必须与条件相关联,该条件必须由代码维护,并且由于它在线程之间共享,因此必须使用互斥锁进行保护。
条件变量与互斥锁相关联,因为这是互斥锁避免竞争的唯一方法。
// incorrect usage:
// thread 1:
while (notDone) {
pthread_mutex_lock(&mutex);
bool ready = protectedReadyToRunVariable
pthread_mutex_unlock(&mutex);
if (ready) {
doWork();
} else {
pthread_cond_wait(&cond1); // invalid syntax: this SHOULD have a mutex
}
}
// signalling thread
// thread 2:
prepareToRunThread1();
pthread_mutex_lock(&mutex);
protectedReadyToRuNVariable = true;
pthread_mutex_unlock(&mutex);
pthread_cond_signal(&cond1);
Now, lets look at a particularly nasty interleaving of these operations
pthread_mutex_lock(&mutex);
bool ready = protectedReadyToRunVariable;
pthread_mutex_unlock(&mutex);
pthread_mutex_lock(&mutex);
protectedReadyToRuNVariable = true;
pthread_mutex_unlock(&mutex);
pthread_cond_signal(&cond1);
if (ready) {
pthread_cond_wait(&cond1); // uh o!
此时,没有线程会向条件变量发出信号,因此thread1将永远等待,即使protectedReadyToRunVariable说它已经准备好了!
解决这个问题的唯一方法是让条件变量原子地释放互斥,同时开始等待条件变量。这就是为什么cond_wait函数需要一个互斥量
// correct usage:
// thread 1:
while (notDone) {
pthread_mutex_lock(&mutex);
bool ready = protectedReadyToRunVariable
if (ready) {
pthread_mutex_unlock(&mutex);
doWork();
} else {
pthread_cond_wait(&mutex, &cond1);
}
}
// signalling thread
// thread 2:
prepareToRunThread1();
pthread_mutex_lock(&mutex);
protectedReadyToRuNVariable = true;
pthread_cond_signal(&mutex, &cond1);
pthread_mutex_unlock(&mutex);
并不是所有的条件变量函数都需要互斥:只有等待操作需要。信号和广播操作不需要互斥。条件变量也不会永久地与特定的互斥锁相关联;外部互斥不保护条件变量。如果一个条件变量具有内部状态,例如等待线程的队列,则必须由条件变量中的内部锁保护。
等待操作将一个条件变量和一个互斥锁结合在一起,因为:
一个线程已经锁定了互斥锁,计算了共享变量的某个表达式,发现它为false,因此需要等待。 线程必须自动地从拥有互斥量转移到等待条件。
出于这个原因,等待操作将互斥量和条件作为参数:这样它就可以管理线程从拥有互斥量到等待的原子转移,这样线程就不会成为丢失唤醒竞争条件的受害者。
A lost wakeup race condition will occur if a thread gives up a mutex, and then waits on a stateless synchronization object, but in a way which is not atomic: there exists a window of time when the thread no longer has the lock, and has not yet begun waiting on the object. During this window, another thread can come in, make the awaited condition true, signal the stateless synchronization and then disappear. The stateless object doesn't remember that it was signaled (it is stateless). So then the original thread goes to sleep on the stateless synchronization object, and does not wake up, even though the condition it needs has already become true: lost wakeup.
条件变量等待函数通过确保调用线程在放弃互斥量之前被注册为可靠地捕获唤醒来避免丢失唤醒。如果条件变量等待函数不接受互斥量作为参数,这是不可能的。
如果你想要一个条件变量的真实例子我在课堂上做了一个练习
#include "stdio.h"
#include "stdlib.h"
#include "pthread.h"
#include "unistd.h"
int compteur = 0;
pthread_cond_t varCond = PTHREAD_COND_INITIALIZER;
pthread_mutex_t mutex_compteur;
void attenteSeuil(arg)
{
pthread_mutex_lock(&mutex_compteur);
while(compteur < 10)
{
printf("Compteur : %d<10 so i am waiting...\n", compteur);
pthread_cond_wait(&varCond, &mutex_compteur);
}
printf("I waited nicely and now the compteur = %d\n", compteur);
pthread_mutex_unlock(&mutex_compteur);
pthread_exit(NULL);
}
void incrementCompteur(arg)
{
while(1)
{
pthread_mutex_lock(&mutex_compteur);
if(compteur == 10)
{
printf("Compteur = 10\n");
pthread_cond_signal(&varCond);
pthread_mutex_unlock(&mutex_compteur);
pthread_exit(NULL);
}
else
{
printf("Compteur ++\n");
compteur++;
}
pthread_mutex_unlock(&mutex_compteur);
}
}
int main(int argc, char const *argv[])
{
int i;
pthread_t threads[2];
pthread_mutex_init(&mutex_compteur, NULL);
pthread_create(&threads[0], NULL, incrementCompteur, NULL);
pthread_create(&threads[1], NULL, attenteSeuil, NULL);
pthread_exit(NULL);
}
我没有发现其他的答案像这一页一样简洁易读。通常等待代码看起来像这样:
mutex.lock()
while(!check())
condition.wait(mutex) # atomically unlocks mutex and sleeps. Calls
# mutex.lock() once the thread wakes up.
mutex.unlock()
将wait()包在互斥锁中有三个原因:
如果没有互斥,另一个线程可能会在wait()之前发出()信号,我们将错过这个唤醒。 通常check()依赖于来自另一个线程的修改,所以无论如何都需要对它进行互斥。 确保优先级最高的线程先进行(互斥锁的队列允许调度器决定谁下一个进行)。
第三点并不总是值得关注的——从文章到对话的历史背景是有联系的。
关于这种机制,经常提到虚假唤醒(即等待线程在没有调用signal()的情况下被唤醒)。但是,这样的事件由循环的check()处理。
这似乎是一个具体的设计决策,而不是概念上的需求。
根据pthreads文档的说法,互斥锁没有被分离的原因是将它们组合在一起可以显著提高性能,并且由于通用的竞争条件,如果你不使用互斥锁,几乎总是会这样做。
https://linux.die.net/man/3/pthread_cond_wait
Features of Mutexes and Condition Variables It had been suggested that the mutex acquisition and release be decoupled from condition wait. This was rejected because it is the combined nature of the operation that, in fact, facilitates realtime implementations. Those implementations can atomically move a high-priority thread between the condition variable and the mutex in a manner that is transparent to the caller. This can prevent extra context switches and provide more deterministic acquisition of a mutex when the waiting thread is signaled. Thus, fairness and priority issues can be dealt with directly by the scheduling discipline. Furthermore, the current condition wait operation matches existing practice.
关于这一点有很多解释,但我想用一个例子来概括一下。
1 void thr_child() {
2 done = 1;
3 pthread_cond_signal(&c);
4 }
5 void thr_parent() {
6 if (done == 0)
7 pthread_cond_wait(&c);
8 }
代码片段有什么问题?在行动前稍作思考。
这个问题真的很微妙。如果父类调用 Thr_parent(),然后检查done的值,它会看到它是0和 因此,去睡觉吧。但就在它呼叫等待睡觉之前,父母 在6-7行之间被中断,并且子程序运行。子进程改变状态变量 Done to 1并发出信号,但没有线程在等待,因此没有线程 中醒来。当父进程再次运行时,它将永远处于休眠状态,这非常令人震惊。
如果它们是在分别获得锁时执行的呢?