разблокировать мьютекс после condition_variable::notify_all() или до?

Просматривая несколько видео и пример документации мы разблокируем мьютекс перед вызовом notify_all(). Будет ли лучше вместо этого называть его после?

Общий способ:

Внутри потока уведомлений:

//prepare data for several worker-threads;

//and now, awaken the threads:
std::unique_lock<std::mutex> lock2(sharedMutex);
_threadsCanAwaken = true;

lock2.unlock(); 
_conditionVar.notify_all(); //awaken all the worker threads;

//wait until all threads completed;

//cleanup:
_threadsCanAwaken = false;

//prepare new batches once again, etc, etc

Внутри одного из рабочих потоков:

while(true){
    // wait for the next batch:

    std::unique_lock<std::mutex> lock1(sharedMutex);
    _conditionVar.wait(lock1,  [](){return _threadsCanAwaken});
    lock1.unlock(); //let sibling worker-threads work on their part as well

    //perform the final task

    //signal the notifier that one more thread has completed;

    //loop back and wait until the next task
}

Обратите внимание, что lock2 разблокируется до того, как мы уведомим переменную условия — должны ли мы вместо этого разблокировать его после notify_all() ?

Изменить

Из моего комментария ниже: Меня беспокоит то, что, если потребитель будет очень быстрым. Потребитель ложно просыпается, видит, что мьютекс разблокирован, завершает задачу и возвращается к началу while. Теперь медлительный производитель, наконец, вызывает notify_all(), в результате чего потребитель зацикливается еще раз.


person Kari    schedule 25.09.2018    source источник
comment
Вызов деструктора lock2 автоматически разблокирует мьютекс после _conditionVar.notify_all();, поэтому вам вообще не нужно вызывать его явно, это обычная идиома IIRC. Кстати, не используйте префикс подчеркивания для любого вашего кода, это зарезервировано для реализаций компилятора и стандартной библиотеки.   -  person πάντα ῥεῖ    schedule 25.09.2018
comment
Спасибо! Меня беспокоит то, что, если потребитель будет очень быстрым. Потребитель ложно просыпается, видит, что мьютекс разблокирован, завершает задачу и возвращается к началу while. Теперь медлительный производитель, наконец, вызывает notify_all(), заставляя потребителя зацикливаться еще раз.   -  person Kari    schedule 25.09.2018


Ответы (4)


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

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

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

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

person David Schwartz    schedule 11.02.2021

должны ли мы вместо этого разблокировать его после notify_all() ?

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

person Slava    schedule 25.09.2018
comment
Правильно ли я понял, что notify_all() освобождает мьютекс от текущей блокировки? Мне это не очень понятно, потому что notify_all() не принимает в качестве аргумента нашу текущую блокировку (в отличие от wait). Я знаю, что наша блокировка будет уничтожена, когда локальная область действия закончится, но не сразу после notify_all ? - person Kari; 25.09.2018
comment
Нет, ни notify_all, ни notify_one не разблокируют мьютекс, это ваша работа либо вручную, либо через механизм RAII. У вас будет другое поведение, если вы разблокируете до или после уведомления (я имею в виду время). - person Slava; 25.09.2018

Как упоминалось здесь: cppreference.com

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

Тем не менее, документация для ожидания

В момент блокировки потока функция автоматически вызывает lck.unlock(), позволяя продолжить выполнение других заблокированных потоков.

После уведомления (явным образом, каким-то другим потоком) функция разблокируется и вызывает lck.lock(), оставляя lck в том же состоянии, в котором была вызвана функция. Затем функция возвращается (обратите внимание, что эта последняя блокировка мьютекса может снова заблокировать поток перед возвратом).

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

Не будьте пессимистами.

person Arpit Singh    schedule 28.07.2019

Меня беспокоит то, что, если потребитель будет очень быстрым. Потребитель ложно просыпается, видит, что мьютекс разблокирован, завершает задачу и возвращается к началу while. Теперь медлительный производитель, наконец, вызывает notify_all(), в результате чего потребитель зацикливается еще раз.

из ссылки на cpp:

Когда уведомляется переменная условия, истекает тайм-аут, или происходит ложное пробуждение, поток пробуждается, и мьютекс повторно захватывается атомарно. Затем поток должен проверить условие и возобновить ожидание, если пробуждение было ложным.

// Manual unlocking is done before notifying, to avoid waking up
// the waiting thread only to block again (see notify_one for details)
lk.unlock();
cv.notify_one();
person Kari    schedule 11.02.2021