首页 文章

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

提问于
浏览
162

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

9 回答

  • 0

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

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

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

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

    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工作中,无论是在开发/服务代码还是作为用户其中,我从未收到其中的一个 . 也许那只是因为惠普有一个不错的实施:-)

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

  • 12

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

    由于技术原因,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);
    }
    

    无法工作,有帮助你,因为你现在将在等待时持有互斥锁 - 即 生产环境 者将永远无法获取互斥锁 . (注意,在这种情况下,您可以创建第二个条件变量来通知 生产环境 者您已完成 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)

  • 55

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

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

  • 172

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

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

    • 一个线程锁定了互斥锁,在共享变量上计算了一些表达式并发现它是假的,这样它就需要等待 .

    • 线程必须 atomically 从拥有互斥锁转移到等待条件 .

    出于这个原因,wait操作将互斥和条件作为参数:这样它就可以管理线程从拥有互斥锁到等待的原子转移,这样线程就不会成为 lost wake up race condition 的牺牲品 .

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

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

  • 3

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

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

  • 3

    条件变量与互斥锁相关联,因为它是避免设计避免的竞争的唯一方法 .

    // 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表示已经准备好了!

    解决此问题的唯一方法是使条件变量 atomically 释放互斥锁,同时开始等待条件变量 . 这就是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

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

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

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

    没有互斥的

    • 另一个线程可以在 wait() 之前 signal() 而且我们会错过这个唤醒 .

    • 通常 check() 依赖于来自另一个线程的修改,所以无论如何你都需要互斥 .

    • 以确保优先级最高的线程首先进行(互斥锁的队列允许调度程序决定下一个是谁) .

    第三点并不总是一个问题 - 历史背景从文章链接到this conversation .

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

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

    它似乎是一个特定的设计决策而不是概念需求 .

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

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

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

相关问题