Поток ожидает условие через Condvar: почему пробуждение не доказывает, что условие уже выполнено?
Пробуждение Condvar — это только сигнал проверить состояние, а не гарантия, что условие истинно. Поток должен повторно проверять предикат под тем же Mutex в цикле; иначе он может продолжить работу слишком рано или пропустить изменение состояния.
Условные переменные появились как механизм эффективного ожидания изменения общего состояния без постоянного опроса и расходования процессорного времени. Они отделяют сам факт изменения состояния от ожидания: Mutex защищает состояние, а Condvar уведомляет ожидающие потоки, что его стоит проверить.
Такой подход решает проблему координации производителя и потребителя, готовности ресурса или завершения работы. Однако условная переменная не хранит логическое условие и не превращает уведомление в надежное событие с накоплением.
Поток может проснуться из-за ложного пробуждения, уведомления другого ожидающего потока или изменения состояния, которое другой поток успел отменить. Даже notify_one означает лишь, что одному потоку следует заново проверить предикат.
Если заменить цикл единичной проверкой, поток может забрать отсутствующий ресурс, обработать пустую очередь или продолжить выполнение до наступления требуемого состояния. Дополнительная проблема — гонка между проверкой условия и началом ожидания: поэтому проверка, передача mutex в ожидание и последующее пробуждение должны выполняться через API условной переменной.
Правильный шаблон таков: захватить mutex, пока предикат ложен — вызвать wait, затем после возврата снова проверить предикат. Condvar::wait атомарно освобождает mutex на время сна и повторно захватывает его перед возвратом, поэтому изменение состояния и переход к ожиданию не разделяются опасным промежутком.
Важен именно предикат состояния, а не значение уведомления. Поток, изменивший ready, должен сделать это под тем же mutex, после чего вызвать notify_one или notify_all; уведомление без согласованного изменения защищаемого состояния не образует надежный протокол.
notify_one обычно эффективнее, когда достаточно разбудить одного потребителя, а notify_all нужен, если изменение может удовлетворить несколько разных ожидающих потоков. Компромисс notify_all — возможное массовое пробуждение потоков, которые затем снова уснут, получив mutex по очереди.
В пуле рабочих потоков очередь задач защищена mutex, а пустая очередь означает необходимость ожидания. Неправильный вариант будит поток и сразу извлекает задачу, полагаясь на уведомление; при ложном пробуждении он получает ошибку или обращается к пустой очереди.
Рассматривались постоянный опрос очереди, ожидание фиксированного времени и Condvar. Опрос прост, но расходует CPU; таймер уменьшает нагрузку, однако добавляет задержку; Condvar не тратит CPU во время ожидания, но требует корректного цикла проверки и единого mutex.
Выбран Condvar с циклической проверкой предиката очередь не пуста или пул завершает работу. Это позволяет корректно отличить новую задачу от завершения и избежать как busy-waiting, так и реакции на ложное пробуждение.
Вопрос: Почему проверять предикат нужно под mutex, даже если само поле читается атомарно?
Ответ: Атомарность отдельного чтения не защищает составной протокол «проверить состояние — заснуть». Между чтением и ожиданием другой поток может изменить состояние и отправить уведомление; без атомарного освобождения mutex вместе с переходом в ожидание поток способен пропустить сигнал. Mutex связывает проверку состояния с постановкой ожидания в единую синхронизационную схему.
Вопрос: Когда предпочтительнее notify_all, а не notify_one?
Ответ: notify_all нужен, когда изменение состояния потенциально делает работу доступной нескольким потокам или когда ожидающие используют разные предикаты на одной условной переменной. Каждый разбуженный поток всё равно обязан проверить свой предикат под mutex. Если доступна только одна задача для одного потребителя, notify_one обычно уменьшает лишние пробуждения.
Вопрос: Может ли уведомление, отправленное до начала ожидания, гарантировать последующее пробуждение?
Ответ: Нет. Condvar не является счетчиком уведомлений и не накапливает сигнал для будущего ожидающего потока. Надежность достигается не сохранением уведомления, а сохранением изменения предиката под mutex: если условие уже стало истинным, следующий поток увидит его при проверке и не будет ждать.