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

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

9 ответов

Решение

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

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

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

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

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 была достойная реализация:-)

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

#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);
}
Другие вопросы по тегам