Программирование GoГорутины и каналыРазработчик Go серверной части

После гарантированно выполненного Signal программа всё равно блокируется на Wait. Какой механизм sync.Cond ...

После гарантированно выполненного Signal программа всё равно блокируется на Wait. Какой механизм sync.Cond это объясняет?

package main

import "sync"

func main() {
	cond := sync.NewCond(&sync.Mutex{})
	done := make(chan struct{})
	go func() {
		cond.L.Lock()
		cond.Signal()
		cond.L.Unlock()
		close(done)
	}()
	<-done
	cond.L.Lock()
	cond.Wait()
	cond.L.Unlock()
}
Проходите собеседования с ИИ помощником Hintsage

Краткий ответ

sync.Cond.Signal не сохраняет уведомление для будущих ожидающих горутин. Если в момент вызова Signal никто не находится внутри Wait, сигнал теряется, поэтому последующий Wait блокируется навсегда.

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

Исторический контекст

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

В отличие от канала, sync.Cond не предназначен для передачи значений или накопления уведомлений. Он связывает ожидание с внешним предикатом, например доступностью места в очереди или переходом состояния объекта.

Постановка проблемы

В примере горутина вызывает Signal, когда ожидающих ещё нет. После этого другая горутина вызывает Wait, но уже не существует события, которое могло бы её разбудить.

Главная ошибка — ожидание уведомления вместо проверки состояния. Даже если заменить порядок операций, одиночный вызов Wait ненадёжен: горутину могут разбудить ложное пробуждение или другой поток, хотя требуемое условие ещё не выполнено.

Подробное решение

Wait должен вызываться при удержании мьютекса. Внутри он атомарно добавляет вызывающую горутину в список ожидающих и освобождает мьютекс; после пробуждения горутина снова захватывает мьютекс и только затем возвращается из Wait.

Состояние проверяют в цикле:

cond := sync.NewCond(&sync.Mutex{}) ready := false go func() { cond.L.Lock() ready = true cond.Broadcast() cond.L.Unlock() }() cond.L.Lock() for !ready { cond.Wait() } cond.L.Unlock()

Изменение ready и его проверка защищены одним мьютексом. Broadcast будит всех текущих ожидающих, а Signal — одного; ни один из них не запоминает уведомление для будущих ожидателей.

Цикл for обязателен: после пробуждения условие могло уже быть изменено другой горутиной. Кроме того, документация допускает возврат из Wait без гарантии, что ожидаемое состояние стало истинным.

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

Ситуация из практики

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

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

Выбранный вариант — очередь под sync.Mutex, ожидание for len(queue) == 0 { cond.Wait() } и Signal или Broadcast после добавления задач. Это не теряет событие, потому что корректность определяется содержимым очереди, а не фактом получения сигнала; результатом становится отсутствие зависаний при гонках между добавлением и ожиданием.

Что кандидаты часто упускают

  1. Можно ли вызвать Wait без предварительной проверки условия?

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

  1. Чем отличаются Signal и Broadcast при нескольких ожидающих горутинах?

Signal будит максимум одну текущую ожидающую горутину, а Broadcast — всех текущих ожидающих. После пробуждения каждая горутина должна заново захватить мьютекс и проверить предикат, поэтому даже после Broadcast часть горутин может снова уснуть.

  1. Почему нельзя изменить предикат после Unlock, а затем вызвать Signal?

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