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

ELL*_*BLE 177 c mutex pthreads condition-variable

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

pax*_*blo 187

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

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

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

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

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.
Run Code Online (Sandbox Code Playgroud)

如果等待返回时有一些可用,则在此循环内完成工作.当线程被标记为停止工作时(通常由另一个线程设置退出条件然后踢条件变量以唤醒该线程),循环将退出,互斥锁将被解锁并且该线程将退出.

上面的代码是单一消费者模型,因为在工作完成时互斥锁保持锁定状态.对于多消费者变体,您可以使用,例如:

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.
Run Code Online (Sandbox Code Playgroud)

这允许其他消费者在这个工作时接收工作.

条件变量减轻了轮询某些条件的负担,反而允许另一个线程在需要发生某些事情时通知您.另一个线程可以告诉该线程工作是否可用,如下所示:

lock mutex.
flag work as available.
signal condition variable.
unlock mutex.
Run Code Online (Sandbox Code Playgroud)

通常错误地称为虚假唤醒的绝大多数通常总是因为多个线程已在其pthread_cond_wait呼叫(广播)中发出信号,一个人将使用互斥锁返回,执行工作,然后重新等待.

然后,当没有工作要做时,第二个发出信号的线程可能会出现.所以你必须有一个额外的变量来表明应该完成工作(这里固有地使用condvar/mutex对进行互斥保护 - 但是在更改之前需要锁定互斥锁的其他线程).

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

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

  • +1用于如何使用互斥锁和condvars的模式; 这就是*总是*完成的.(好吧,除了做错了......) (15认同)
  • @stefaanv"互斥体仍然是为了保护条件变量,没有其他方法可以保护它":互斥体是*不*来保护条件变量; 它是为了保护*谓词数据*,但我想你知道通过阅读那句话之后的评论.你可以合法地发出一个条件变量,并且完全支持实现,*post*-unlock of mutex包装谓词,实际上你将*在某些情况下*缓解*争用. (7认同)
  • '做某事'不应该在while循环中.你希望你的while循环只是检查条件,否则如果你得到一个虚假的唤醒,你也可能会"做点什么". (3认同)
  • @WhozCraig,+1,是的,互斥锁不是为了保护条件变量。 (3认同)
  • 可能我说的不够清楚。循环_不_等待工作准备就绪,以便您可以完成。循环是主要的“无限”工作循环。如果您从 cond_wait 返回并且设置了工作标志,则您完成工作然后再次循环。“while some condition”仅在您希望线程停止工作时才会为假,此时它将释放互斥锁并最有可能退出。 (2认同)

nos*_*nos 57

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

由于技术原因,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);
}
Run Code Online (Sandbox Code Playgroud)

你自然会得到很多竞争条件,如果另一个线程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);
}
Run Code Online (Sandbox Code Playgroud)

无法工作,在唤醒和抓住互斥锁之间仍然存在竞争条件的可能性.在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);
}
Run Code Online (Sandbox Code Playgroud)

(生产者自然需要采取相同的预防措施,始终使用相同的互斥锁保护'some_data',并确保如果some_data当前不会覆盖some_data!= NULL)

  • 我只是偶然发现了这个问题,坦率地说,发现这个答案是正确的,这个答案比paxdiablo的答案要少得多,这个答案有明确的缺陷(原子性仍然需要,只有处理条件需要互斥量,不得处理或通知).我想这就是stackoverflow的工作方式...... (4认同)
  • 不,你真正在等待的是'some_data'是非空的.如果它是非空的"第一次",很好,你拿着互斥锁并且可以安全地使用数据.如果你有一个do/while循环,你会错过通知,如果有人在你等待之前发出了条件变量的信号(这与在win32上找到的事件没有任何关系,直到有人等待它们才会发出信号) (3认同)

Dav*_*rtz 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
}
Run Code Online (Sandbox Code Playgroud)

看看线程如何等待工作.这项工作受互斥锁保护.等待释放互斥锁,以便另一个线程可以给这个线程一些工作.这是如何发出信号:

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);
}
Run Code Online (Sandbox Code Playgroud)

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

  • 或者,更简洁地说,条件变量的全部要点是提供原子“解锁和等待”操作。如果没有互斥锁,就没有什么可以解锁的。 (3认同)

Kaz*_*Kaz 13

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

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

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

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

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

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

  • @snr 无状态同步对象不记得任何与信号相关的状态。当有信号时,如果现在有东西在等待它,它被唤醒,否则唤醒被遗忘。条件变量像这样是无状态的。根据正确编写的逻辑,使同步可靠的必要状态由应用程序维护,并由与条件变量结合使用的互斥锁保护。 (2认同)

Sam*_*man 6

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

mutex.lock()
while(!check())
    condition.wait()
mutex.unlock()
Run Code Online (Sandbox Code Playgroud)

wait()包含互斥锁的原因有三个:

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

第三点并不总是一个问题 - 历史背景从文章到这个对话都有联系.

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


Cor*_*ica 5

条件变量与互斥体相关联,因为它是避免竞争的唯一方法,它旨在避免。

// 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!
Run Code Online (Sandbox Code Playgroud)

此时,没有线程会向条件变量发出信号,因此线程 1 将永远等待,即使 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);
Run Code Online (Sandbox Code Playgroud)