Какую роль играет один и тот же мьютекс при изменении предиката и ожидании на std::condition_variable?
Один и тот же мьютекс делает проверку предиката, его изменение и переход потока к ожиданию частью согласованного протокола. Благодаря этому не возникает гонки, при которой поток проверил предикат, но ещё не начал ждать, а другой поток уже изменил состояние и отправил уведомление.
Само уведомление не хранится в std::condition_variable: если оно произошло до фактического ожидания, оно может быть потеряно. Поэтому ждать нужно в цикле с повторной проверкой предиката, обычно через перегрузку wait с предикатом.
Условные переменные появились как механизм эффективного ожидания изменения состояния без активного опроса памяти. Поток временно освобождает мьютекс и засыпает, а после уведомления снова получает мьютекс и проверяет состояние.
Ключевая проблема такого подхода — гонка между последней проверкой условия и началом ожидания. Атомарная связка «освободить мьютекс и начать ждать» устраняет это окно, если все изменения предиката выполняются в том же протоколе синхронизации.
Пусть предикат означает, что очередь не пуста. Потребитель проверяет его и видит пустую очередь. Если производитель изменит очередь и уведомит потребителя до того, как тот реально начнёт ожидать, уведомление не будет поставлено в очередь на будущее.
Без общего мьютекса потребитель может заснуть навсегда или проснуться без гарантии, что состояние действительно подходит для продолжения. Дополнительно возможны ложные пробуждения, поэтому одного факта пробуждения недостаточно.
Изменение предиката выполняют под мьютексом. Ожидающий поток также захватывает этот мьютекс, проверяет предикат и вызывает wait; библиотека атомарно освобождает мьютекс и переводит поток в состояние ожидания.
Производитель захватывает тот же мьютекс, изменяет защищаемое состояние и затем уведомляет ожидающих. Уведомление можно выполнить до или после освобождения мьютекса, но предикат должен быть изменён согласованно с ним.
После пробуждения поток сначала повторно захватывает мьютекс, а затем проверяет предикат. Поэтому корректный шаблон — wait с предикатом или цикл while, а не одиночный вызов wait.
Использование другого мьютекса для изменения предиката разрушает эту гарантию: проверка состояния и постановка в ожидание больше не образуют единого протокола. Атомарный предикат иногда позволяет построить альтернативную схему, но тогда корректность должна обеспечиваться явным протоколом на основе атомиков; сама condition_variable не делает произвольные операции согласованными.
В многопоточном обработчике задач потребитель сначала проверял размер очереди без мьютекса, а затем захватывал мьютекс только для извлечения элемента. При высокой нагрузке производитель мог добавить задачу и уведомить потребителя между этими действиями. Потребитель после этого засыпал, хотя очередь уже содержала работу.
Возможный вариант — постоянно опрашивать атомарный счётчик задач. Он проще для редких изменений состояния, но создаёт лишнюю нагрузку, задержки и сложность выбора интервала опроса. Другой вариант — использовать condition_variable, но защищать счётчик и очередь разными мьютексами; это не устраняет гонку между проверкой и ожиданием.
Выбранное решение — единый мьютекс для очереди и предиката, wait с условием и уведомление после добавления задачи. Такой протокол не теряет состояние, не тратит процессор на активное ожидание и позволяет безопасно обрабатывать ложные пробуждения.
notify_one, чтобы ожидающий поток гарантированно продолжил работу?Нет. Уведомление лишь делает поток кандидатом на пробуждение. Поток продолжит работу только после повторного захвата мьютекса, а затем должен убедиться, что предикат истинен. Если условие ложно, он снова ожидает.
Такой порядок обычно некорректен: ожидающий поток может проснуться, увидеть старое ложное состояние и снова уснуть, после чего изменение предиката уже не вызовет нового уведомления. Сначала нужно изменить состояние, затем уведомить; само уведомление обычно выполняют под мьютексом или сразу после его освобождения.
Нет, это не универсальное требование. Уведомление под мьютексом корректно, но разбуженный поток может немедленно попытаться захватить ещё занятый мьютекс. Уведомление после освобождения часто уменьшает такую лишнюю конкуренцию, однако между изменением предиката и уведомлением нельзя допускать нарушение протокола, зависящее от конкретного мьютекса.