Answers:
这是条件变量(或最初)实现的方式。
互斥锁用于保护条件变量本身。这就是为什么在等待之前需要将其锁定。
等待将“原子地”解锁互斥锁,从而允许其他人访问条件变量(用于发出信号)。然后,当信号通知或广播条件变量时,将唤醒等待列表中的一个或多个线程,并且该线程将再次神奇地锁定互斥锁。
您通常会看到以下带有条件变量的操作,说明了它们的工作方式。以下示例是一个工作线程,该工作线程通过信号发送给条件变量以进行工作。
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 / mutex对进行互斥保护的-其他线程需要在更改互斥之前锁定互斥体)。
这是技术上是可行的一个线程从条件等待而不被其他进程被踢回(这是一个真正的虚假唤醒),但是,在所有我多年的并行线程的工作,无论是在开发/代码的服务,并为用户其中,我从来没有收到过其中之一。也许仅仅是因为惠普实施得当:-)
在任何情况下,处理错误情况的相同代码也可以处理真正的虚假唤醒,因为不会为这些情况设置工作可用标志。
do something
在while
循环内?
如果只能发出条件信号,那么条件变量将受到很大的限制,通常您需要处理一些与发出信号的条件有关的数据。信号/唤醒必须在不引入竞争条件的情况下以原子方式完成,或者过于复杂
出于相当技术上的原因,pthreads还可以给您带来虚假的唤醒。这意味着您需要检查一个谓词,以便可以确定条件实际上已发出信号-并将其与虚假唤醒区分开。检查与等待有关的这种条件需要加以保护-因此条件变量需要一种在锁定/解锁保护该条件的互斥锁时自动等待/唤醒的方法。
考虑一个简单的示例,在该示例中会通知您一些数据已生成。也许另一个线程生成了所需的一些数据,并设置了指向该数据的指针。
想象一个生产者线程通过“ 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当前为!= NULL,则不会覆盖some_data)
while (some_data != NULL)
do-while循环,以便它至少等待一次条件变量?
while(some_data != NULL)
应该while(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);
}
请注意,您需要互斥体来保护工作队列。注意,条件变量本身不知道是否有用。也就是说,条件变量必须与条件相关联,该条件必须由您的代码维护,并且由于它在线程之间共享,因此必须由互斥体保护。
并非所有条件变量函数都需要互斥量:只有等待操作才需要。信号和广播操作不需要互斥体。条件变量也不是与特定的互斥体永久关联的。外部互斥锁不保护条件变量。如果条件变量具有内部状态,例如等待线程的队列,则必须通过条件变量内部的内部锁来保护它。
等待操作将条件变量和互斥锁放在一起,因为:
因此,wait操作将互斥量和condition用作参数:这样它就可以管理从拥有互斥体到等待状态的线程的原子转移,从而使线程不会成为丢失的唤醒竞争条件的受害者。
如果线程放弃了互斥锁,然后等待无状态同步对象,但是以一种非原子的方式,将会丢失唤醒竞争条件:存在一个时间窗口,当线程不再具有锁并且具有尚未开始等待该对象。在此窗口期间,另一个线程可以进入,使等待的条件为真,发出无状态同步信号,然后消失。无状态对象不记得它已发出信号(它是无状态的)。因此,原始线程将进入无状态同步对象的睡眠状态,并且不会唤醒,即使它所需要的条件已经变为真:丢失的唤醒。
条件变量等待功能通过确保在放弃互斥之前确保已注册调用线程以可靠地捕获唤醒,从而避免丢失唤醒。如果条件变量等待功能未将互斥锁作为参数,则这将是不可能的。
我没有找到其他答案像本页一样简洁明了。通常,等待代码如下所示:
mutex.lock()
while(!check())
condition.wait()
mutex.unlock()
将其包装wait()
在互斥锁中有三个原因:
signal()
在之前出现wait()
,我们会错过这种唤醒的机会。check()
取决于其他线程的修改,因此无论如何您都需要相互排斥。第三点并不总是令人担忧的-从文章到本次对话都链接了历史背景。
关于这种机制,通常会提到虚假的唤醒(即,等待线程被唤醒而不signal()
被调用)。但是,此类事件由looped处理check()
。
条件变量与互斥锁相关联,因为它是唯一可以避免本应避免的竞争的方法。
// 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!
此时,没有线程会向状态变量发出信号,因此即使protectedReadyToRunVariable表示已准备就绪,线程1将永远等待。
解决此问题的唯一方法是让条件变量自动释放互斥锁,同时开始等待条件变量。这就是为什么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);
互斥锁应该在您调用时被锁定pthread_cond_wait
;当您调用它时,它会自动解锁互斥锁,然后根据条件进行阻止。发出条件信号后,它将自动再次锁定并返回。
如果需要的话,这允许实现可预测的调度,因为将要进行信号传递的线程可以等待,直到互斥体被释放以进行其处理,然后发出条件信号。
如果您想要条件变量的真实示例,我在课堂上做了一个练习:
#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);
}
这似乎是一个特定的设计决策,而不是概念上的需求。
根据pthreads docs,互斥锁未分离的原因是通过将它们组合在一起可以显着提高性能,并且他们希望,由于不使用互斥锁而在常见的竞争条件下,无论如何几乎都会这样做。
https://linux.die.net/man/3/pthread_cond_wait
互斥体和条件变量的功能
有人建议互斥体的获取和释放与条件等待分离。这被拒绝,因为实际上是操作的组合性质有助于实时实现。这些实现可以以对调用者透明的方式在条件变量和互斥锁之间原子地移动高优先级线程。这样可以防止进行额外的上下文切换,并在发出等待线程的信号时提供互斥性的确定性获取。因此,调度规则可以直接处理公平性和优先级问题。此外,当前条件等待操作与现有实践相匹配。
关于这一点有很多见解,但我想通过以下示例概括一下。
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
为1
并发出信号,但是没有线程在等待,因此没有线程被唤醒。当父母再次奔跑时,它会永远睡觉,这真是令人难以置信。
如果在单独获得锁的情况下进行密码锁怎么办?