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


182

我正在阅读pthread.h; 与条件变量相关的函数(如pthread_cond_wait(3))需要互斥体作为参数。为什么?至于我可以告诉大家,我将要创建一个互斥只是为这样的说法用?该互斥锁应该做什么?

Answers:


194

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

互斥锁用于保护条件变量本身。这就是为什么在等待之前需要将其锁定。

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

您通常会看到以下带有条件变量的操作,说明了它们的工作方式。以下示例是一个工作线程,该工作线程通过信号发送给条件变量以进行工作。

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对进行互斥保护的-其他线程需要在更改互斥之前锁定互斥体)。

技术上是可行的一个线程从条件等待而不被其他进程被踢回(这是一个真正的虚假唤醒),但是,在所有我多年的并行线程的工作,无论是在开发/代码的服务,并为用户其中,我从来没有收到过其中之一。也许仅仅是因为惠普实施得当:-)

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


3
“做某事”不应该在while循环内。您希望while循环仅检查条件,否则,如果您收到虚假的唤醒,也可能会“做某事”。

1
不,错误处理仅次于此。使用pthread,可以在没有明显原因(虚假唤醒)的情况下被唤醒,并且没有任何错误。因此,您醒来后需要重新检查“某些情况”。

1
我不确定我是否理解。我的反应与否定相同; 为什么do somethingwhile循环内?
ELLIOTTCABLE

1
也许我还不够清楚。循环不是等待工作准备就绪,您就可以这样做。该循环是主要的“无限”工作循环。如果您从cond_wait返回并且设置了工作标志,那么您将进行工作,然后再次循环。仅当您希望线程停止工作时,“有条件的情况”才会为false,此时线程将释放互斥量并很可能退出。
paxdiablo

7
@stefaanv“互斥锁仍在保护条件变量,没有其他方法可以保护它”:互斥锁不是在保护条件变量;这是为了保护谓词数据,但我认为您可以通过阅读声明后的注释来了解这一点。你可以合法信号的条件变量,并通过实现,完全支持互斥包装谓词的-unlock,而事实上你将缓解在某些情况下,这样的竞争。
WhozCraig 2013年

59

如果只能发出条件信号,那么条件变量将受到很大的限制,通常您需要处理一些与发出信号的条件有关的数据。信号/唤醒必须在不引入竞争条件的情况下以原子方式完成,或者过于复杂

出于相当技术上的原因,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循环,以便它至少等待一次条件变量?
Maygarden法官10年

3
不。您真正在等待的是'some_data'为非null。如果“首次”不为空,那就太好了,您持有互斥量并且可以安全地使用数据。如果你有一个做/ while循环,你会错过了通知,如果有人示意条件变量,你就可以等待(它的不一样的事件上保持有信号,直到有人等待他们的win32中)之前

4
我只是偶然发现了这个问题,坦率地说,很奇怪地发现,这个答案是正确的,与paxdiablo的答案相比要少得多,而paxdiablo的答案有一定的缺陷(仍然需要原子性,互斥量仅用于处理这种情况,不用于处理或通知)。我想这就是stackoverflow的工作原理
stefaanv

@stefaanv,如果您想详细说明这些缺陷,请作为对我的答案的评论,以便我及时看到它们,而不是几个月后:-),我将很乐意修复它们。您的简短短语并没有真正给我足够的细节来阐明您要说的话。
paxdiablo

1
@nos,不while(some_data != NULL)应该while(some_data == NULL)吗?
埃里克Z

30

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);
}

请注意,您需要互斥体来保护工作队列。注意,条件变量本身不知道是否有用。也就是说,条件变量必须与条件相关联,该条件必须由您的代码维护,并且由于它在线程之间共享,因此必须由互斥体保护。


1
或者,简而言之,条件变量的全部要点是提供原子的“解锁并等待”操作。没有互斥锁,将无法解锁。
David Schwartz

您介意解释无状态的含义吗?
snr

@snr他们没有任何状态。它们不是“锁定”或“有信号”或“无信号”。因此,您有责任跟踪与条件变量关联的任何状态。例如,如果条件变量让线程知道队列何时变为非空,则必须是一个线程可以使队列变为非空,而其他一些线程需要知道队列何时变为非空。那是共享状态,您必须使用互斥锁保护它。您可以将条件变量与受互斥锁保护的共享状态相关联,用作唤醒机制。
大卫·史瓦兹

16

并非所有条件变量函数都需要互斥量:只有等待操作才需要。信号和广播操作不需要互斥体。条件变量也不是与特定的互斥体永久关联的。外部互斥锁不保护条件变量。如果条件变量具有内部状态,例如等待线程的队列,则必须通过条件变量内部的内部锁来保护它。

等待操作将条件变量和互斥锁放在一起,因为:

  • 线程已锁定互斥锁,对共享变量求值了一些表达式,发现该表达式为假,因此需要等待。
  • 线程必须从拥有互斥体原子地移动到等待条件。

因此,wait操作将互斥量和condition用作参数:这样它就可以管理从拥有互斥体到等待状态的线程的原子转移,从而使线程不会成为丢失的唤醒竞争条件的受害者。

如果线程放弃了互斥锁,然后等待无状态同步对象,但是以一种非原子的方式,将会丢失唤醒竞争条件:存在一个时间窗口,当线程不再具有锁并且具有尚未开始等待该对象。在此窗口期间,另一个线程可以进入,使等待的条件为真,发出无状态同步信号,然后消失。无状态对象不记得它已发出信号(它是无状态的)。因此,原始线程将进入无状态同步对象的睡眠状态,并且不会唤醒,即使它所需要的条件已经变为真:丢失的唤醒。

条件变量等待功能通过确保在放弃互斥之前确保已注册调用线程以可靠地捕获唤醒,从而避免丢失唤醒。如果条件变量等待功能未将互斥锁作为参数,则这将是不可能的。


您能否提供广播操作不需要获取互斥锁的参考?在MSVC上,广播将被忽略。
xvan

@xvan POSIX pthread_cond_broadcastpthread_cond_signal操作(这个SO问题所涉及的)甚至都没有将互斥锁作为参数;只有条件。POSIX规范在这里。仅在等待线程唤醒时提及它们时提及互斥锁。
卡兹(Kaz)

您介意解释无状态的含义吗?
snr

1
@snr无状态同步对象不记得任何与信令相关的状态。发出信号时,如果正在等待某物,则将其唤醒,否则将忘记唤醒。这样的条件变量是无状态的。根据正确编写的逻辑,使同步可靠的必要状态由应用程序维护,并由互斥体保护,该互斥体与条件变量一起使用。
哈兹(Kaz)

7

我没有找到其他答案像本页一样简洁明了。通常,等待代码如下所示:

mutex.lock()
while(!check())
    condition.wait()
mutex.unlock()

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

  1. 没有互斥锁,另一个线程可能会signal()在之前出现wait(),我们会错过这种唤醒的机会。
  2. 通常check()取决于其他线程的修改,因此无论如何您都需要相互排斥。
  3. 以确保优先级最高的线程优先进行(互斥体的队列使调度程序可以决定谁继续执行)。

第三点并不总是令人担忧的-从文章到本次对话都链接了历史背景。

关于这种机制,通常会提到虚假的唤醒(即,等待线程被唤醒而不signal()被调用)。但是,此类事件由looped处理check()


4

条件变量与互斥锁相关联,因为它是唯一可以避免本应避免的竞争的方法。

// 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);

3

互斥锁应该在您调用时被锁定pthread_cond_wait;当您调用它时,它会自动解锁互斥锁,然后根据条件进行阻止。发出条件信号后,它将自动再次锁定并返回。

如果需要的话,这允许实现可预测的调度,因为将要进行信号传递的线程可以等待,直到互斥体被释放以进行其处理,然后发出条件信号。


所以……对于我来说,有没有理由不让互斥锁始终处于解锁状态,而是在等待之前将其锁定,然后在等待完成之后立即将其解锁?
ELLIOTTCABLE

互斥锁还解决了等待线程和信令线程之间的某些潜在竞争。只要在更改条件和信号通知时互斥锁始终处于锁定状态,您就永远不会发现自己错过了信号并永远休眠
Hasturkun 2010年

所以…… 在等待conditionvar之前,我应该在conditionvar的互斥锁上等待互斥量?我不确定我是否完全理解。
ELLIOTTCABLE

2
@elliottcable:如果不持有互斥锁,您怎么知道是否应该等待?如果您还在等什么什么刚才发生了什么?
大卫·史瓦兹

1

如果您想要条件变量的真实示例,我在课堂上做了一个练习:

#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);
}

1

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

根据pthreads docs,互斥锁未分离的原因是通过将它们组合在一起可以显着提高性能,并且他们希望,由于不使用互斥锁而在常见的竞争条件下,无论如何几乎都会这样做。

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

互斥体和条件变量的功能

有人建议互斥体的获取和释放与条件等待分离。这被拒绝,因为实际上是操作的组合性质有助于实时实现。这些实现可以以对调用者透明的方式在条件变量和互斥锁之间原子地移动高优先级线程。这样可以防止进行额外的上下文切换,并在发出等待线程的信号时提供互斥性的确定性获取。因此,调度规则可以直接处理公平性和优先级问题。此外,当前条件等待操作与现有实践相匹配。


0

关于这一点有很多见解,但我想通过以下示例概括一下。

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并发出信号,但是没有线程在等待,因此没有线程被唤醒。当父母再次奔跑时,它会永远睡觉,这真是令人难以置信。

如果在单独获得锁的情况下进行密码锁怎么办?

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.