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

时间:2010-05-04 08:05:01

标签: c pthreads mutex condition-variable

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

10 个答案:

答案 0 :(得分:179)

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

互斥锁用于保护条件变量本身。这就是你在等待之前需要锁定它的原因。

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

您通常会看到以下有条件变量的操作,说明它们的工作原理。以下示例是一个工作线程,它通过信号传递给条件变量。

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

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

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

答案 1 :(得分:56)

如果您只能发出条件信号,则条件变量非常有限,通常您需要处理与信号发送条件相关的某些数据。信号/唤醒必须以原子方式完成,以便在不引入竞争条件的情况下实现,或者过于复杂

由于技术原因,pthreads还可以为您提供spurious wakeup。这意味着您需要检查谓词,这样您就可以确定实际上已经发出了信号 - 并将其与虚假唤醒区分开来。检查等待它的这种情况需要加以保护 - 所以一个条件变量需要一种原子等待/唤醒的方法,同时锁定/解锁保护该条件的互斥锁。

考虑一个简单的示例,其中通知您生成了一些数据。也许另一个线程制作了你想要的一些数据,并设置了指向该数据的指针。

想象一下,生产者线程通过'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

之前做了什么

您无法真正创建自己的互斥锁以保护此案例.e.g

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)

答案 2 :(得分:29)

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

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

答案 3 :(得分:13)

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

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

  • 一个线程锁定了互斥锁,在共享变量上计算了一些表达式并发现它是假的,这样它就需要等待。
  • 线程必须原子从拥有互斥锁转移到等待条件。

由于这个原因,wait操作将互斥和条件作为参数:这样它就可以管理线程从拥有互斥锁到等待的原子转移,这样线程就不会成为的牺牲品失去了醒来的竞争状态

如果一个线程放弃一个互斥锁,然后等待一个无状态同步对象,但是以一种非原子的方式,将会发生丢失的唤醒竞争条件:当线程不再具有锁定时,存在一个时间窗口,还没有开始等待对象。在此窗口期间,另一个线程可以进入,使等待条件成立,发出无状态同步信号然后消失。无状态对象不记得它被发出信号(它是无状态的)。那么原始线程就会在无状态同步对象上进入休眠状态,并且不会唤醒,即使它所需的条件已经成为现实:丢失唤醒。

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

答案 4 :(得分:6)

我发现其他答案与this page一样简洁易读。通常,等待代码看起来像这样:

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

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

  1. 在没有互斥锁的情况下,signal()之前另一个主题可以wait(),我们会错过这个醒来。
  2. 通常check()依赖于另一个线程的修改,所以无论如何你都需要互斥。
  3. 确保优先级最高的线程首先进行(互斥锁的队列允许调度程序决定下一个是谁)。
  4. 第三点并不总是令人担忧 - 历史背景与文章相关联this conversation

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

答案 5 :(得分: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!

此时,没有线程会发出条件变量的信号,因此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);

答案 6 :(得分:3)

当您拨打pthread_cond_wait时,互斥锁应该被锁定;当你调用它时,它原子地解锁互斥锁,然后阻止条件。一旦条件发出信号,它就会再次以原子方式锁定它并返回。

如果需要,这允许实现可预测的调度,因为正在执行信令的线程可以等到释放互斥锁进行处理然后发出信号。

答案 7 :(得分: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);
}

答案 8 :(得分:1)

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

根据pthreads文档,互斥锁未被分离的原因是,通过组合它们可以显着提高性能,并且他们希望由于常见的竞争条件,如果您不使用互斥锁,它会使用互斥锁。无论如何,几乎总是要做。

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

  

互斥锁和条件变量的特征

     

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

答案 9 :(得分: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并发出信号,但没有线程在等待,因此没有线程 醒了。父母再次奔跑时,它会永远睡着,这真是令人难以置信。

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