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

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

Ответ 1

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

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

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

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

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

Тогда вторая сигнальная нить могла выйти, когда не было никакой работы. Таким образом, у вас должна быть дополнительная переменная, указывающая, что работа должна быть выполнена (это было по сути защищено mutex с помощью пары condvar/mutex здесь - другие потоки, необходимые для блокировки мьютекса, прежде чем изменять его, однако).

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

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

Ответ 2

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

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)

Ответ 3

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

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

Ответ 4

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

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

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

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

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

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

Ответ 5

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

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

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

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

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

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

Ответ 6

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

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

Ответ 7

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

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

Ответ 8

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

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

Ответ 9

Это, по-видимому, конкретное дизайнерское решение, а не концептуальная потребность.

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

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

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

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

Ответ 10

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

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