C语言 为什么 pthread 的条件变量函数需要互斥锁?

声明:本页面是StackOverFlow热门问题的中英对照翻译,遵循CC BY-SA 4.0协议,如果您需要使用它,必须同样遵循CC BY-SA许可,注明原文地址和作者信息,同时你必须将它归于原作者(不是我):StackOverFlow 原文地址: http://stackoverflow.com/questions/2763714/
Warning: these are provided under cc-by-sa 4.0 license. You are free to use/share it, But you must attribute it to the original authors (not me): StackOverFlow

提示:将鼠标放在中文语句上可以显示对应的英文。显示中英文
时间:2020-09-02 05:21:52  来源:igfitidea点击:

Why do pthreads’ condition variable functions require a mutex?

cpthreadsmutexcondition-variable

提问by ELLIOTTCABLE

I'm reading up on pthread.h; the condition variable related functions (like pthread_cond_wait(3)) require a mutex as an argument. Why? As far as I can tell, I'm going to be creating a mutex justto use as that argument? What is that mutex supposed to do?

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

回答by paxdiablo

It's just the way that condition variables are (or were originally) implemented.

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

The mutex is used to protect the condition variable itself. That's why you need it locked before you do a wait.

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

The wait will "atomically" unlock the mutex, allowing others access to the condition variable (for signalling). Then when the condition variable is signalled or broadcast to, one or more of the threads on the waiting list will be woken up and the mutex will be magically locked again for that thread.

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

You typically see the following operation with condition variables, illustrating how they work. The following example is a worker thread which is given work via a signal to a condition variable.

您通常会看到以下带有条件变量的操作,说明它们是如何工作的。下面的例子是一个工作线程,它通过一个条件变量的信号来工作。

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.

The work is done within this loop provided that there is some available when the wait returns. When the thread has been flagged to stop doing work (usually by another thread setting the exit condition then kicking the condition variable to wake this thread up), the loop will exit, the mutex will be unlocked and this thread will exit.

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

The code above is a single-consumer model as the mutex remains locked while the work is being done. For a multi-consumer variation, you can use, as an example:

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

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.

which allows other consumers to receive work while this one is doing work.

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

The condition variable relieves you of the burden of polling some condition instead allowing another thread to notify you when something needs to happen. Another thread can tell that thread that work is available as follows:

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

lock mutex.
flag work as available.
signal condition variable.
unlock mutex.

The vast majority of what are often erroneously called spurious wakeups was generally always because multiple threads had been signalled within their pthread_cond_waitcall (broadcast), one would return with the mutex, do the work, then re-wait.

绝大多数通常被错误地称为虚假唤醒通常总是因为多个线程在它们的pthread_cond_wait调用(广播)中发出了信号,一个会带着互斥锁返回,完成工作,然后重新等待。

Then the second signalled thread could come out when there was no work to be done. So you had to have an extra variable indicating that work should be done (this was inherently mutex-protected with the condvar/mutex pair here - other threads needed to lock the mutex before changing it however).

然后当没有工作要做时,第二个有信号的线程可以出来。因此,您必须有一个额外的变量来指示应该完成的工作(这在本质上是通过 condvar/mutex 对进行互斥保护的——但是,其他线程需要在更改互斥之前锁定互斥)。

It wastechnically possible for a thread to return from a condition wait without being kicked by another process (this is a genuine spurious wakeup) but, in all my many years working on pthreads, both in development/service of the code and as a user of them, I never once received one of these. Maybe that was just because HP had a decent implementation :-)

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

In any case, the same code that handled the erroneous case also handled genuine spurious wakeups as well since the work-available flag would not be set for those.

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

回答by nos

A condition variable is quite limited if you could only signal a condition, usually you need to handle some data that's related to to condition that was signalled. Signalling/wakeup have to be done atomically in regards to achieve that without introducing race conditions, or be overly complex

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

pthreads can also give you , for rather technical reasons, a spurious wakeup. That means you need to check a predicate, so you can be sure the condition actually was signalled - and distinguish that from a spurious wakeup. Checking such a condition in regards to waiting for it need to be guarded - so a condition variable needs a way to atomically wait/wake up while locking/unlocking a mutex guarding that condition.

由于技术原因,pthreads 还可以为您提供虚假唤醒。这意味着您需要检查谓词,以便您可以确定条件实际上已发出信号 - 并将其与虚假唤醒区分开来。检查与等待有关的条件需要受到保护 - 因此条件变量需要一种在锁定/解锁保护该条件的互斥锁时自动等待/唤醒的方法。

Consider a simple example where you're notified that some data are produced. Maybe another thread made some data that you want, and set a pointer to that data.

考虑一个简单的例子,你会收到一些数据被生成的通知。也许另一个线程制作了一些你想要的数据,并设置一个指向该数据的指针。

Imagine a producer thread giving some data to another consumer thread through a 'some_data' pointer.

想象一个生产者线程通过“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);
}

you'd naturally get a lot of race condition, what if the other thread did some_data = new_dataright after you got woken up, but before you did data = some_data

你自然会得到很多竞争条件,如果另一个线程some_data = new_data在你醒来后立即做了,但在你做之前data = some_data

You cannot really create your own mutex to guard this case either .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);
}

Will not work, there's still a chance of a race condition in between waking up and grabbing the mutex. Placing the mutex before the pthread_cond_wait doesn't help you, as you will now hold the mutex while waiting - i.e. the producer will never be able to grab the mutex. (note, in this case you could create a second condition variable to signal the producer that you're done with some_data- though this will become complex, especially so if you want many producers/consumers.)

行不通,在醒来和获取互斥锁之间仍有可能出现竞争条件。将互斥锁放在 pthread_cond_wait 之前对您没有帮助,因为您现在将在等待时持有互斥锁 - 即生产者将永远无法获取互斥锁。(注意,在这种情况下,您可以创建第二个条件变量来通知生产者您已经完成了some_data- 尽管这会变得复杂,尤其是如果您想要许多生产者/消费者。)

Thus you need a way to atomically release/grab the mutex when waiting/waking up from the condition. That's what pthread condition variables does, and here's what you'd do:

因此,您需要一种在等待/从条件中唤醒时自动释放/获取互斥锁的方法。这就是 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);
}

(the producer would naturally need to take the same precautions, always guarding 'some_data' with the same mutex, and making sure it doesn't overwrite some_data if some_data is currently != NULL)

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

回答by David Schwartz

POSIX condition variables are stateless. So it is your responsibility to maintain the state. Since the state will be accessed by both threads that wait and threads that tell other threads to stop waiting, it must be protected by a mutex. If you think you can use condition variables without a mutex, then you haven't grasped that condition variables are stateless.

POSIX 条件变量是无状态的。因此,维护状态是您的责任。由于状态将被等待的线程和告诉其他线程停止等待的线程访问,它必须由互斥锁保护。如果您认为可以在没有互斥锁的情况下使用条件变量,那么您还没有理解条件变量是无状态的。

Condition variables are built around a condition. Threads that wait on a condition variable are waiting for some condition. Threads that signal condition variables change that condition. For example, a thread might be waiting for some data to arrive. Some other thread might notice that the data has arrived. "The data has arrived" is the condition.

条件变量是围绕条件构建的。等待条件变量的线程正在等待某个条件。发出条件变量信号的线程会改变该条件。例如,一个线程可能正在等待某些数据到达。其他一些线程可能会注意到数据已经到达。“数据已到达”是条件。

Here's the classic use of a condition variable, simplified:

这是条件变量的经典用法,简化了:

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
}

See how the thread is waiting for work. The work is protected by a mutex. The wait releases the mutex so that another thread can give this thread some work. Here's how it would be signalled:

查看线程如何等待工作。该工作受互斥锁保护。等待释放互斥体,以便另一个线程可以为该线程提供一些工作。以下是它的信号方式:

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

Notice that you needthe mutex to protect the work queue. Notice that the condition variable itself has no idea whether there's work or not. That is, a condition variable mustbe associated with a condition, that condition must be maintained by your code, and since it's shared among threads, it must be protected by a mutex.

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

回答by Kaz

Not all condition variable functions require a mutex: only the waiting operations do. The signal and broadcast operations do not require a mutex. A condition variable also is not permanently associated with a specific mutex; the external mutex does not protect the condition variable. If a condition variable has internal state, such as a queue of waiting threads, this must be protected by an internal lock inside the condition variable.

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

The wait operations bring together a condition variable and a mutex, because:

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

  • a thread has locked the mutex, evaluated some expression over shared variables and found it to be false, such that it needs to wait.
  • the thread must atomicallymove from owning the mutex, to waiting on the condition.
  • 一个线程锁定了互斥锁,对共享变量计算了一些表达式,发现它是假的,因此它需要等待。
  • 线程必须原子地从拥有互斥锁转移到等待条件。

For this reason, the wait operation takes as arguments both the mutex and condition: so that it can manage the atomic transfer of a thread from owning the mutex to waiting, so that the thread does not fall victim to the lost wake up race condition.

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

A lost wakeup race condition will occur if a thread gives up a mutex, and then waits on a stateless synchronization object, but in a way which is not atomic: there exists a window of time when the thread no longer has the lock, and has not yet begun waiting on the object. During this window, another thread can come in, make the awaited condition true, signal the stateless synchronization and then disappear. The stateless object doesn't remember that it was signaled (it is stateless). So then the original thread goes to sleep on the stateless synchronization object, and does not wake up, even though the condition it needs has already become true: lost wakeup.

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

The condition variable wait functions avoid the lost wake up by making sure that the calling thread is registered to reliably catch the wakeup before it gives up the mutex. This would be impossible if the condition variable wait function did not take the mutex as an argument.

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

回答by Sam Brightman

I do not find the other answers to be as concise and readable as this page. Normally the waiting code looks something like this:

我没有发现其他答案像本页那样简洁易读。通常,等待代码如下所示:

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

There are three reasons to wrap the wait()in a mutex:

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

  1. without a mutex another thread could signal()before the wait()and we'd miss this wake up.
  2. normally check()is dependent on modification from another thread, so you need mutual exclusion on it anyway.
  3. to ensure that the highest priority thread proceeds first (the queue for the mutex allows the scheduler to decide who goes next).
  1. 如果没有互斥锁,另一个线程可以signal()在 之前wait(),我们会错过这个唤醒。
  2. 通常check()依赖于来自另一个线程的修改,因此无论如何您都需要对其进行互斥。
  3. 以确保最高优先级的线程首先执行(互斥锁的队列允许调度程序决定谁接下来执行)。

The third point is not always a concern - historical context is linked from the article to this conversation.

第三点并不总是值得关注的——历史背景是从文章到这次对话的链接

Spurious wake-ups are often mentioned with regard to this mechanism (i.e. the waiting thread is awoken without signal()being called). However, such events are handled by the looped check().

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

回答by Cort Ammon

Condition variables are associated with a mutex because it is the only way it can avoid the race that it is designed to avoid.

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

// 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!

At this point, there is no thread which is going to signal the condition variable, so thread1 will wait forever, even though the protectedReadyToRunVariable says it's ready to go!

此时,没有线程会向条件变量发出信号,因此线程 1 将永远等待,即使 protectedReadyToRunVariable 表示它已准备就绪!

The only way around this is for condition variables to atomicallyrelease the mutex while simultaneously starting to wait on the condition variable. This is why the cond_wait function requires a mutex

解决这个问题的唯一方法是让条件变量原子地释放互斥锁,同时开始等待条件变量。这就是 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);

回答by Amber

The mutex is supposed to be locked when you call pthread_cond_wait; when you call it it atomically both unlocks the mutex and then blocks on the condition. Once the condition is signaled it atomically locks it again and returns.

当您调用时,互斥锁应该被锁定pthread_cond_wait;当您以原子方式调用它时,它都会解锁互斥锁,然后在条件上阻塞。一旦条件发出信号,它会再次自动锁定并返回。

This allows the implementation of predictable scheduling if desired, in that the thread that would be doing the signalling can wait until the mutex is released to do its processing and then signal the condition.

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

回答by Catskul

It appears to be a specific design decision rather than a conceptual need.

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

Per the pthreads docs the reason that the mutex was not separated is that there is a significant performance improvement by combining them and they expect that because of common race conditions if you don't use a mutex, it's almost always going to be done anyway.

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

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

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

Features of Mutexes and Condition Variables

It had been suggested that the mutex acquisition and release be decoupled from condition wait. This was rejected because it is the combined nature of the operation that, in fact, facilitates realtime implementations. Those implementations can atomically move a high-priority thread between the condition variable and the mutex in a manner that is transparent to the caller. This can prevent extra context switches and provide more deterministic acquisition of a mutex when the waiting thread is signaled. Thus, fairness and priority issues can be dealt with directly by the scheduling discipline. Furthermore, the current condition wait operation matches existing practice.

互斥体和条件变量的特点

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

回答by Central Thinking Unit

I made an exercice in class if you want a real example of condition variable :

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

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

回答by snr

There are a tons of exegeses about that, yet I want to epitomize it with an example following.

关于这一点有很多注释,但我想用下面的例子来概括它。

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 }

What's wrong with the code snippet? Just ponder somewhat before going ahead.

代码片段有什么问题?在继续之前稍微思考一下。



The issue is genuinely subtle. If the parent invokes thr_parent()and then vets the value of done, it will see that it is 0and thus try to go to sleep. But just before it calls wait to go to sleep, the parent is interrupted between lines of 6-7, and the child runs. The child changes the state variable doneto 1and signals, but no thread is waiting and thus no thread is woken. When the parent runs again, it sleeps forever, which is really egregious.

这个问题真的很微妙。如果父级调用 thr_parent()然后 的值done,它将看到它是0,因此尝试进入睡眠状态。但是就在它调用wait进入睡眠之前,父进程在6-7行之间被打断,子进程运行。孩子将状态变量更改 done1和信号,但没有线程在等待,因此没有线程被唤醒。当父进程再次运行时,它会永远休眠,这真是令人震惊。

What if they are carried out while acquired locks individually?

如果在单独获取锁时执行它们会怎样?