Когда требуется переменная условия, не достаточно ли мьютекса?

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

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

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

Я запутался в том, что поток также может ожидать мьютекс, и когда он получает сигнал, это просто означает, что переменная теперь доступна, зачем мне нужна условная переменная?

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

8 ответов

Решение

Несмотря на то, что вы можете использовать их так, как вы описываете, мьютексы не были предназначены для использования в качестве механизма уведомления / синхронизации. Они предназначены для обеспечения взаимоисключающего доступа к общему ресурсу. Использование мьютексов для сигнализации о состоянии неудобно, и я полагаю, что это будет выглядеть примерно так (где Thread1 сигнализируется Thread2):

Резьба1:

while(1) {
    lock(mutex); // Blocks waiting for notification from Thread2
    ... // do work after notification is received
    unlock(mutex); // Tells Thread2 we are done
}

Резьба2:

while(1) {
    ... // do the work that precedes notification
    unlock(mutex); // unblocks Thread1
    lock(mutex); // lock the mutex so Thread1 will block again
}

Есть несколько проблем с этим:

  1. Поток 2 не может продолжать "выполнять работу, предшествующую уведомлению", пока поток 1 не завершит работу "после уведомления". При таком дизайне Thread2 даже не нужен, то есть почему бы не перенести "работу, которая предшествует", и "работу после уведомления" в один и тот же поток, поскольку только один из них может выполняться в данный момент времени!
  2. Если Thread2 не может вытеснить Thread1, Thread1 немедленно повторно блокирует мьютекс, когда он повторяет цикл while(1), и Thread1 будет выполнять "работу после уведомления", даже если уведомления не было. Это означает, что вы должны как-то гарантировать, что Thread2 заблокирует мьютекс раньше, чем Thread1. Как ты это делаешь? Может быть, вызвать событие расписания с помощью сна или с помощью других средств, специфичных для ОС, но даже это не гарантированно сработает в зависимости от времени, вашей ОС и алгоритма планирования.

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

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

Mutex предназначен для монопольного доступа к общему ресурсу, а условная переменная - для ожидания выполнения условия. Люди могут подумать, что они могут реализовать условные переменные без поддержки ядра. Распространенное решение, которое можно придумать, это "флаг + мьютекс":

lock(mutex)

while (!flag) {
    sleep(100);
}

unlock(mutex)

do_something_on_flag_set();

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

Я тоже думал об этом, и самая важная информация, которую я пропускал повсюду, заключалась в том, что мьютекс может иметь (и изменять) в то время только один поток. Таким образом, если у вас есть один производитель и больше потребителей, производитель должен будет ждать мьютекса для производства. С конд. переменную он может произвести в любое время.

Вам нужны условные переменные, которые будут использоваться с мьютексом (каждый cond.var. Принадлежит мьютексу), чтобы сигнализировать об изменении состояний (условий) от одного потока к другому. Идея состоит в том, что поток может ждать, пока какое-либо условие не станет истинным. Такие условия зависят от программы (т. Е. "Очередь пуста", "матрица большая", "некоторый ресурс почти исчерпан", "какой-то этап вычисления завершен" и т. Д.). Мьютекс может иметь несколько связанных переменных условия. И вам нужны переменные условия, потому что такие условия не всегда могут быть выражены так же просто, как "мьютекс заблокирован" (поэтому вам нужно передавать изменения условий в другие потоки).

Прочитайте несколько хороших учебных пособий по Posix, например, это руководство или то или другое. А еще лучше прочитайте хорошую книгу. Смотрите этот вопрос.

Также читайте Расширенное программирование Unix и Расширенное программирование Linux

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

Условная переменная var и пара мьютексов могут быть заменены парой двоичного семафора и мьютекса. Последовательность операций потока потребителя при использовании условного var + mutex:

  1. Заблокировать мьютекс

  2. Подожди на условной переменной

  3. Процесс

  4. Разблокировать мьютекс

Последовательность операций потока производителя

  1. Заблокировать мьютекс

  2. Сигнал условный вар

  3. Разблокировать мьютекс

Соответствующая последовательность потока потребителя при использовании пары sema+mutex имеет вид

  1. Жди бинарного сема

  2. Заблокировать мьютекс

  3. Проверьте на ожидаемое состояние

  4. Если условие верно, обработайте.

  5. Разблокировать мьютекс

  6. Если проверка условия на шаге 3 была ложной, вернитесь к шагу 1.

Последовательность для потока производителя:

  1. Заблокировать мьютекс

  2. Опубликовать двоичную сема

  3. Разблокировать мьютекс

Как вы можете видеть, безусловная обработка на шаге 3 при использовании условной переменной заменяется условной обработкой на шаге 3 и шаге 4 при использовании двоичной sema.

Причина в том, что при использовании sema+mutex в состоянии гонки другой потребительский поток может проникнуть между шагами 1 и 2 и обработать / обнулить условие. Этого не произойдет при использовании условной переменной. При использовании условной переменной условие гарантированно будет истинным после шага 2.

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

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

      import threading, time, random
cv = threading.Condition()
buffer = []
MAX = 3

def put(value):    
    cv.acquire()
    while len(buffer) == MAX:            
        cv.wait()        
    buffer.append(value)
    print("added value ", value, "length =", len(buffer))
    cv.notify()
    cv.release()

def get():    
    cv.acquire()
    while len(buffer) == 0:            
        cv.wait()        
    value = buffer.pop()
    print("removed value ", value, "length =", len(buffer))
    cv.notify()
    cv.release()

def producer():
    while True:
        put(0) # it doesn't mater what is the value in our example
        time.sleep(random.random()/10)

def consumer():
    while True:
        get()
        time.sleep(random.random()/10)

if __name__ == '__main__':
    cs = threading.Thread(target=consumer)    
    pd = threading.Thread(target=producer)
    cs.start()            
    pd.start()
    cs.join()
    pd.join()

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

Как упоминалось в http://en.cppreference.com/w/cpp/thread/mutex/unlock,

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

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

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


Например, вы можете исправить случай @slowjelj с помощью дополнительного мьютекса (это может быть неправильное исправление):

Резьба1:

lock(mutex0);
while(1) {
    lock(mutex0); // Blocks waiting for notification from Thread2
    ... // do work after notification is received
    unlock(mutex1); // Tells Thread2 we are done
}

Резьба2:

while(1) {
    lock(mutex1); // lock the mutex so Thread1 will block again
    ... // do the work that precedes notification
    unlock(mutex0); // unblocks Thread1
}

Но ваша программа будет жаловаться, что вы сработали с утверждением, оставленным компилятором (например, "разблокировка неизвестного мьютекса" в Visual Studio 2015).

На мой взгляд, возможно, вы можете использоватьtwo mutexреализоватьmutex + cond_var

вот способ:

  • заменятьpthread_cond_wait(&cond_var, &mutex)с
      pthread_mutex_unlock(&mutex);
pthread_mutex_trylock(&mutex_new);
pthread_mutex_lock(&mutex_new);
pthread_mutex_lock(&mutex);
  • заменятьpthread_cond_signal(&cond_var)с
      pthread_mutex_unlock(&mutex_new);

но есть еще проблема, возможно производитель это сделаетsignalвещи между

      pthread_mutex_unlock(&mutex);
producer signal!
pthread_mutex_trylock(&mutex_new);

тогда потребитель никогда не проснется, ноconditional varне позволю этому случиться

Другие вопросы по тегам