Почему функции условных переменных pthreads требуют мьютекса?

182

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

ELLIOTTCABLE
источник

Ответы:

194

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

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

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

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

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, как в развитии услуг / коды и как пользователь из них я ни разу не получил ни одного из них. Может быть, это только потому, что у HP была достойная реализация :-)

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

paxdiablo
источник
3
«делать что-то» не должно быть внутри цикла while. Вы бы хотели, чтобы ваш цикл while просто проверял состояние, в противном случае вы могли бы также «сделать что-то», если вы получили ложное пробуждение.
1
нет, обработка ошибок занимает второе место. С помощью pthreads вы можете проснуться без видимой причины (ложное пробуждение) и безо всякой ошибки. Таким образом, вам нужно перепроверить «некоторое состояние» после того, как вы проснулись.
1
Я не уверен, что понимаю. У меня была такая же реакция, как у nos ; почему do somethingвнутри whileцикла?
ELLIOTTCABLE
1
Возможно, я недостаточно проясняю. Цикл не состоит в том, чтобы ждать, пока работа будет готова, чтобы вы могли это сделать. Цикл является основным «бесконечным» рабочим циклом. Если вы вернетесь из cond_wait и установите флаг работы, вы выполняете работу, а затем снова зацикливаетесь. «while some condition» будет ложным только тогда, когда вы хотите, чтобы поток прекратил работу, и в этот момент он освободит мьютекс и, скорее всего, выйдет.
paxdiablo
7
@stefaanv "мьютекс по-прежнему защищает условную переменную, другого способа защитить его нет": мьютекс не защищает условную переменную; это для защиты предикатных данных , но я думаю, что вы знаете об этом, прочитав ваш комментарий, который последовал за этим утверждением. Вы можете сигнализировать переменную условия на законных основаниях и полностью поддерживаться реализациями, после разблокировки мьютекса, обертывающего предикат, и фактически вы в некоторых случаях уменьшите конфликт при этом.
WhozCraig
59

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

По техническим причинам 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

Вы не можете создать свой собственный мьютекс, чтобы защитить это дело .eg

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)

н.у.к.
источник
Не должен ли while (some_data != NULL)быть цикл do-while, чтобы он ожидал условную переменную хотя бы один раз?
судья Мейгарден
3
Нет. То, что вы действительно ждете, это чтобы некоторые_данные были ненулевыми. Если значение «null» не равно нулю, отлично, вы держите мьютекс и можете безопасно использовать данные. Если бы у вас был цикл do / while, вы пропустили бы уведомление, если бы кто-то сигнализировал переменную условия до того, как вы ее ожидали (это не что иное, как события, обнаруженные на win32, которые остаются сигнальными до тех пор, пока кто-то их не ждет)
nos
4
Я просто наткнулся на этот вопрос, и, честно говоря, странно обнаружить, что этот правильный ответ, который является правильным, имеет гораздо меньше точек, чем ответ Паксдиабло, который имеет определенные недостатки (атомарность все еще необходима, мьютекс нужен только для обработки условия, не для обработки или уведомления). Я полагаю, именно так работает stackoverflow ...
stefaanv
@stefaanv, если вы хотите подробно описать недостатки, как комментарии к моему ответу, чтобы я видел их своевременно, а не месяцы спустя :-), я буду рад их исправить. Ваши краткие фразы не дают мне достаточно подробностей, чтобы понять, что вы пытаетесь сказать.
paxdiablo
1
@nos, не должно while(some_data != NULL)быть while(some_data == NULL)?
Эрик З
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
}

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

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

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

Дэвид Шварц
источник
1
Или, чтобы выразиться более кратко, весь смысл условных переменных состоит в том, чтобы обеспечить атомарную операцию «разблокировки и ожидания». Без мьютекса было бы нечего открывать.
Дэвид Шварц
Не могли бы вы объяснить значение слова « без гражданства» ?
ОСШ
@snr У них нет государства. Они не «заблокированы» или «сигнализированы» или «не сигнализированы». Поэтому вы несете ответственность за отслеживание состояния, связанного с переменной условия. Например, если переменная условия позволяет потоку узнать, когда очередь становится непустой, это должен быть случай, когда один поток может сделать очередь непустой, а некоторый другой поток должен знать, когда очередь становится непустой. Это общее состояние, и вы должны защитить его мьютексом. Вы можете использовать условную переменную в сочетании с этим общим состоянием, защищенным мьютексом, в качестве механизма пробуждения.
Дэвид Шварц
16

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

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

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

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

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

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

Kaz
источник
Не могли бы вы указать, что операции широковещательной передачи не требуют получения мьютекса? На MSVC трансляция игнорируется.
xvan
@xvan POSIX pthread_cond_broadcastи pthread_cond_signalоперации (о которых этот вопрос SO) даже не принимают мьютекс в качестве аргумента; только условие. Спецификация POSIX здесь . Мьютекс упоминается только в отношении того, что происходит в ожидающих потоках, когда они просыпаются.
Kaz
Не могли бы вы объяснить значение слова « без гражданства» ?
ОСШ
1
@snr Объект синхронизации без сохранения состояния не запоминает состояния, связанные с сигнализацией. При получении сигнала, если что-то ждет его сейчас, оно просыпается, иначе пробуждение забыто. Переменные условия не имеют состояния, как это. Необходимое состояние для обеспечения надежной синхронизации поддерживается приложением и защищается мьютексом, который используется вместе с переменными условия в соответствии с правильно написанной логикой.
Каз
7

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

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

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

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

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

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

Сэм Брайтман
источник
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);
Корт Аммон
источник
3

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

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

янтарный
источник
Итак ... есть ли у меня причина не просто оставлять мьютекс всегда разблокированным, а затем блокировать его прямо перед ожиданием, а затем разблокировать сразу после окончания ожидания?
ELLIOTTCABLE
Мьютекс также решает некоторые потенциальные расы между ожидающим и сигнальным потоками. до тех пор, пока мьютекс всегда блокируется при изменении состояния и сигнализации, вы никогда не пропустите сигнал и не будете спать вечно
Hasturkun
Итак… я должен сначала подождать мьютекс на мьютексе условного переменной, прежде чем ждать на условном переменной? Я не уверен, что понимаю вообще.
ELLIOTTCABLE
2
@elliottcable: Не держа мьютекс, как ты мог знать, стоит ли ждать или нет? Что, если то, что вы ждете, только что произошло?
Дэвид Шварц
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);
}
Центральное мышление
источник
1

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

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

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

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

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

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

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

ОСШ
источник