为什么pthreads的条件变量函数需要互斥?

我正在阅读pthread.h;与条件变量相关的函数(如pthread_cond_wait(3))需要一个互斥量作为参数。为什么?据我所知,我将创建一个互斥量只是用作参数?互斥锁应该做什么?

86916 次浏览

这只是条件变量(或最初)实现的方式。

互斥锁用于保护条件变量本身。这就是为什么你在等待之前要把它锁上。

等待将“原子地”解锁互斥锁,允许其他人访问条件变量(用于发送信号)。然后,当条件变量被发送信号或广播到时,等待列表上的一个或多个线程将被唤醒,互斥锁将神奇地再次为该线程锁定。

您通常会看到以下使用条件变量的操作,说明它们是如何工作的。下面的例子是一个工作线程,它通过信号将工作分配给一个条件变量。

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/互斥锁对固有的互斥锁保护的-其他线程需要在改变互斥锁之前锁定它)。

从技术上讲,线程从条件等待中返回而不被另一个进程踢出是可能的(这是一个真正的虚假唤醒),但是,在我多年的pthreads工作中,无论是在代码的开发/服务中,还是作为他们的用户,我从来没有收到过其中的一个。也许这只是因为惠普有一个不错的实现:-)

在任何情况下,处理错误情况的相同代码也处理了真正的虚假唤醒,因为不会为它们设置工作可用标志。

当调用pthread_cond_wait时,互斥锁应该是锁定的;当你调用它时,它会自动地解锁互斥锁,然后在条件上阻塞。一旦条件发出信号,它就会自动锁定它并返回。

这允许在需要的情况下实现可预测的调度,因为发送信号的线程可以等到互斥锁释放后再进行处理,然后发出条件信号。

一个条件变量是非常有限的,如果你只能信号一个条件,通常你需要处理一些数据,相关的条件被信号。信号/唤醒必须在不引入竞态条件的情况下实现,或者过于复杂

出于技术上的原因,pthreads也可以给你一个虚假的唤醒。这意味着您需要检查一个谓词,这样您就可以确保该条件确实发出了信号——并将其与虚假唤醒区分开来。检查这样一个条件是否等待它需要被保护——所以一个条件变量需要一种方法来原子地等待/唤醒,同时锁定/解锁一个保护该条件的互斥锁。

考虑一个简单的例子,在这个例子中,您被通知生成了一些数据。也许另一个线程创建了一些你想要的数据,并设置了指向该数据的指针。

想象一个生产者线程通过'some_data'将一些数据传递给另一个消费者线程 指针。< / p >
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,因此需要等待。
  • 线程必须自动从拥有互斥量转移到等待条件。

因此,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);
}

我不认为其他答案像这个页面一样简洁易读。通常等待代码看起来像这样:

mutex.lock()
while(!check())
condition.wait(mutex) # atomically unlocks mutex and sleeps. Calls
# mutex.lock() once the thread wakes up.
mutex.unlock()

wait()包装在互斥锁中有三个原因:

  1. 如果没有互斥,另一个线程可以在wait()之前signal(),我们将错过这个唤醒。
  2. 通常check()依赖于来自另一个线程的修改,所以无论如何你都需要对它进行互斥。
  3. 确保优先级最高的线程先进行(互斥锁的队列允许调度器决定谁下一个进行)。

第三点并不总是值得关注——历史上下文从文章链接到这次谈话

关于这种机制,经常提到虚假唤醒(即等待线程在没有调用signal()的情况下被唤醒)。然而,这样的事件是由循环的check()

这似乎是一个具体的设计决策,而不是概念上的需求。

根据pthreads文档的说法,互斥锁没有被分离的原因是将它们组合在一起可以显著提高性能,并且由于通用的竞争条件,如果你不使用互斥锁,几乎总是会这样做。

https://linux.die.net/man/3/pthread_cond_wait

互斥对象和条件变量的特性

曾经有人建议互斥量的获取和释放应该是 与条件等待解耦。这个被拒绝了,因为它是 结合操作的本质,实际上,方便了实时性 实现。这些实现可以原子地移动 对象中条件变量和互斥量之间的高优先级线程 对调用方透明的方式。这可以防止额外的 上下文切换并提供更确定的互斥量获取 当等待线程发出信号时。因此,公平和优先 问题可以通过调度规程直接处理。 此外,当前条件等待操作与现有操作匹配 练习。< / p >

关于这一点有很多解释,但我想用一个例子来概括一下。

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行之间被中断,并且子程序运行。子进程改变状态变量 done1和信号,但没有线程正在等待,因此没有线程 中醒来。当父进程再次运行时,它将永远处于休眠状态,这是非常糟糕的

如果它们是在分别获得锁时执行的呢?