# Condition variable, или как сделать все правильно и уйти в deadlock Синхронизация потоков это сложно, хотя у нас и есть примитивы. Такой себе каламбур. Хорошо, если есть готовые высокоуровневые абстракции в виде очередей или каналов. Но иногда приходится мастерить их самому. С использованием более низкоуровневых вещей: мьютексов, атомарных переменных и обвязки вокруг них. `condition_variable` — примитив синхронизации, позволяющий одному или нескольким потокам ожидать сообщений от других потоков. Ожидать пассивно, не тратя время CPU впустую на постоянные проверки чего-то в цикле. Поток просто снимается с исполнения, ставится в очередь операционной системой, а по наступлении определенного события — уведомления — от другого потока пробуждается. Все замечательно и удобно. Сам по себе примитив `condition_variable` не передает никакой информации, а только служит для пробуждения или усыпления потоков. Причем пробуждения, из-за особенностей реализации блокировок, могут случаться ложно, самопроизвольно (spurious), а не только лишь по непосредственной команде через `condition_variable`. Потому типичное использование требует дополнительной проверки условий и выглядит как-то так. ```C++ std::condition_variable cv; std::mutex event_mutex; bool event_happened = false; // исполняется в одном потоке void task1() { std::unique_lock lock { event_mutex }; // предикат гарантированно проверяется под захваченной блокировкой cv.wait(lock, [&] { return event_happened; }); // безпредикатная версия wait ждет только уведомления, // но может произойти ложное пробуждения (обычно, если кто-то отпускает этот же мьютекс) ... // дождались — событие наступило // выполняем что нужно } // исполняется в другом потоке void task2() { ... { std::lock_guard lock {event_mutex}; event_happened = true; } // Обратите внимание: вызов notify не обязан быть под захваченной блокировкой. // Однако, в ранних версиях msvc, а также в очень старой версии из boost были // баги, требующие удерживать мьютекс захваченным во время вызова notify() // Но есть случай, когда делать вызов notify под блокировкой необходимо — если // другой тред может вызвать, например, завершаясь, деструктор объекта cv cv.notify_one(); // notify_all() } ``` Хм, внимательный читатель может сообразить, что в task2 мьютекс используется только для защиты булевого флага. Невиданное расточительство! Целых два системных вызова в худшем случае. Давайте лучше флаг сделаем атомарным! ```C++ std::atomic_bool event_happened = false; std::condition_variable cv; std::mutex event_mutex; void task1() { std::unique_lock lock { event_mutex }; cv.wait(lock, [&] { return event_happened; }); ... } void task2() { ... event_happened = true; cv.notify_one(); // notify_all() ... } ``` Компилируем, запускаем, работает — классно, срочно в релиз! Но однажды приходит пользователь и говорит, что запустил task1 и task2 как обычно одновременно, но сегодня внезапно task1 не завершается, хотя task2 отработал! Вы идете к пользователю, смотрите — висит. Перезапускаете — не зависает. Еще перезапускаете — опять не зависает. Перезапускаете 50 раз — все равно не зависает. Сбой какой-то железный разовый, думаете вы. Уходите. Через месяц пользователь опять приходит с той же проблемой. И опять не воспроизводится. Ну точно железный сбой, космическая радиация битик какой-то в локальном кэше треда выбивает. Ничего страшного... На самом деле в программе ошибка, приводящая к блокировке при редком совпадении в порядке инструкций. Чтобы понять ее, нужно посмотреть внимательнее на то, как устроен метод `wait` с предикатом. ```C++ // thread a std::unique_lock lock {event_mutex}; // a1 // cv.wait(lock, [&] { return event_happened; }) это while (![&] { return event_happened; }()) { // a2 cv.wait(lock); // a3 } // ------------------------------- // thread b event_happened = true; // b1 cv.notify_one(); // b2 ``` Рассмотрим следующую последовательность исполнения строчек в двух потоках ``` a1 // поток 1 захватывает блокировку a2 // поток 1 проверяет условие --- истинно, событие не наступило b1 // поток 2 выставляет событие, b2 // уведомляет о нем, но поток 1 еще не начал ждать! уведомление потеряно! a3 // поток 1 начинает ждать и никогда не дождется! ``` Отоптимизировали? Возвращайте захват мьютекса обратно. Захват мьютекса в уведомляющем потоке гарантирует, что ожидающий уведомления поток либо еще не начал ждать и проверять событие, либо уже ждет. Если же мьютекс не захвачен, мы можем попасть в промежуточное состояние. Осторожнее с примитивами! ## Полезные ссылки 1. https://en.cppreference.com/w/cpp/thread/condition_variable 2. https://stackoverflow.com/questions/2531359/do-condition-variables-still-need-a-mutex-if-youre-changing-the-checked-value-a/2531397#2531397 3. https://www.modernescpp.com/index.php/c-core-guidelines-be-aware-of-the-traps-of-condition-variables