Почему функции условных переменных pthreads требуют мьютекса? - PullRequest
172 голосов
/ 04 мая 2010

Я читаю на pthread.h; функции, связанные с переменной условия (например, pthread_cond_wait(3)), требуют мьютекса в качестве аргумента. Зачем? Насколько я могу судить, я собираюсь создать мьютекс просто для использования в качестве этого аргумента? Что должен делать этот мьютекс?

Ответы [ 10 ]

180 голосов
/ 04 мая 2010

Это просто способ, которым условные переменные (или были изначально) реализованы.

Мьютекс используется для защиты самой переменной условия . Вот почему вам нужно заблокировать его перед ожиданием.

Ожидание «атомно» разблокирует мьютекс, предоставляя другим доступ к переменной условия (для сигнализации). Затем, когда переменная условия будет сигнализирована или передана, один или несколько потоков в списке ожидания будут разбужены, и мьютекс снова будет магически заблокирован для этого потока.

Обычно вы видите следующую операцию с условными переменными, иллюстрирующую, как они работают. В следующем примере показан рабочий поток, которому дается работа через сигнал условной переменной.

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 - другие потоки должны были заблокировать мьютекс перед тем, как его изменить).

* * * * * * * * * * * * * * * * * * * * технически возможно *1030* для потока, чтобы вернуться из условия ожидания, не будучи запущенным другим процессом (это - подлинное ложное пробуждение), но, за все мои многие годы работы над pthreads, оба в разработке / службы кода и, как пользователь их, я никогда не получал один из них. Может быть, это потому, что у HP была достойная реализация: -)

В любом случае, тот же код, который обрабатывал ошибочный случай, также обрабатывал подлинные ложные пробуждения, так как для них не был установлен флаг доступности работы.

56 голосов
/ 04 мая 2010

Переменная условия весьма ограничена, если вы можете сигнализировать только об условии, обычно вам нужно обработать некоторые данные, которые относятся к условию, о котором было сообщено. Сигнализация / пробуждение должны быть сделаны атомарно, чтобы достичь этого без введения условий гонки, или быть слишком сложными

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

Вы также не можете создать свой собственный мьютекс для защиты этого дела .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)

29 голосов
/ 27 августа 2011

Переменные условия 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);
}

Обратите внимание, что вам нужен мьютекс для защиты рабочей очереди. Обратите внимание, что сама переменная условия не имеет представления, есть ли работа или нет. То есть переменная условия должна быть связана с условием, это условие должно поддерживаться вашим кодом, а поскольку оно совместно используется потоками, оно должно быть защищено мьютексом.

13 голосов
/ 25 апреля 2014

Не все функции условных переменных требуют мьютекса: только операции ожидания. Сигнал и широковещательные операции не требуют мьютекса. Переменная условия также не постоянно связана с конкретным мьютексом; внешний мьютекс не защищает переменную условия. Если переменная условия имеет внутреннее состояние, например очередь ожидающих потоков, это должно быть защищено внутренней блокировкой внутри переменной условия.

Операции ожидания объединяют переменную условия и мьютекс, потому что:

  • поток заблокировал мьютекс, оценил некоторое выражение над общими переменными и обнаружил, что оно ложно, так что ему нужно подождать.
  • поток должен атомарно перейти от владения мьютексом к ожиданию условия.

По этой причине операция ожидания принимает в качестве аргументов как мьютекс, так и условие: так, чтобы она могла управлять атомарным переносом потока из владения мьютексом в ожидание, чтобы поток не становился жертвой состояние проигранной гонки на пробуждение .

Потерянное состояние гонки при пробуждении произойдет, если поток откажется от мьютекса, а затем ожидает объект синхронизации без состояния, но не атомарным способом: существует окно времени, когда поток больше не имеет блокировки и еще не начал ждать на объекте. Во время этого окна может войти другой поток, выполнить ожидаемое условие, сообщить о синхронизации без сохранения состояния и затем исчезнуть. Объект без состояния не помнит, что ему было сообщено (он не имеет состояния). Таким образом, исходный поток переходит в спящий режим на объекте синхронизации без сохранения состояния и не просыпается, даже если необходимое ему условие уже стало истинным: потерянное пробуждение.

Функции ожидания условной переменной позволяют избежать потерянного пробуждения, убедившись, что вызывающий поток зарегистрирован, чтобы надежно перехватить пробуждение, прежде чем он откажется от мьютекса. Это было бы невозможно, если бы функция ожидания условной переменной не принимала мьютекс в качестве аргумента.

6 голосов
/ 29 июля 2016

Я не считаю другие ответы такими же краткими и удобочитаемыми, как эта страница . Обычно код ожидания выглядит примерно так:

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

Есть три причины, чтобы обернуть wait() в мьютекс:

  1. без мьютекса другой поток может signal() до wait(), и мы пропустим это пробуждение.
  2. обычно check() зависит от модификации из другого потока, поэтому вам все равно нужно взаимное исключение.
  3. для обеспечения того, чтобы поток с наивысшим приоритетом продолжался первым (очередь для мьютекса позволяет планировщику решать, кто будет следующим).

Третий момент не всегда вызывает беспокойство - исторический контекст связан со статьей с этим разговором .

В отношении этого механизма часто упоминаются ложные пробуждения (то есть ожидающий поток пробуждается без вызова signal()). Однако такие события обрабатываются зацикленным check().

4 голосов
/ 04 сентября 2013

Переменные условия связаны с мьютексом, потому что это единственный способ избежать гонки, для которой он предназначен.

// 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);
3 голосов
/ 04 мая 2010

Мьютекс должен быть заблокирован при вызове pthread_cond_wait; когда вы вызываете его, он атомарно разблокирует мьютекс, а затем блокирует условие. Как только условие сигнализируется, оно снова блокирует его и возвращает.

Это позволяет реализовать предсказуемое планирование, если это необходимо, так как поток, который будет выполнять сигнализацию, может ожидать освобождения мьютекса, чтобы выполнить его обработку, а затем сигнализировать о состоянии.

1 голос
/ 25 октября 2017

Похоже, что это конкретное проектное решение, а не концептуальная потребность.

Согласно документам pthreads, причина того, что мьютекс не был разделен, заключается в том, что благодаря их объединению происходит значительное улучшение производительности, и они ожидают, что из-за общих условий гонки, если вы не используете мьютекс, это почти всегда будет сделано в любом случае.

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

Особенности мьютексов и переменных состояния

Было высказано предположение, что приобретение и освобождение мьютекса отделен от состояния ожидания. Это было отклонено, потому что это комбинированный характер операции, который, по сути, облегчает в реальном времени Реализации. Эти реализации могут атомарно перемещать высокоприоритетный поток между условной переменной и мьютексом в манера, которая прозрачна для звонящего. Это может предотвратить дополнительные переключение контекста и обеспечение более детерминированного получения мьютекса когда ожидающий поток сигнализируется. Таким образом, справедливость и приоритет вопросы могут быть решены непосредственно с помощью дисциплины планирования. Кроме того, текущая операция ожидания состояния соответствует существующей практика.

1 голос
/ 04 января 2015

Я сделал упражнение в классе, если вы хотите реальный пример условной переменной:

#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);
}
0 голосов
/ 02 мая 2019

Существует множество толкований об этом, но я хочу выразить это следующим примером.

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, и, таким образом, попытается заснуть.Но непосредственно перед вызовом wait для засыпания родитель прерывается между строками 6-7, а потом бежит.Дочерний объект изменяет переменную состояния done на 1 и сигнализирует, но никакой поток не ожидает и, таким образом, ни один поток не пробуждается.Когда родитель снова бежит, он спит вечно, что действительно вопиюще.

Что если они выполняются, когда приобретаются блокировки по отдельности?

...