4.0Kпросмотров
22 ноября 2025 г.
stats📷 ФотоScore: 4.4K
💥sync package. Cond's hidden trap
В этот раз поговорим про sync.Cond и убедимся, что этот примитив небезопасно использовать без оглядки на важную штуку... Одна забытая проверка — и ваши горутины повиснут навечно, инициируя OOM Killed 1. Preface
Cond — примитив синхронизации для ожидания услови(я|ий) без busy-waiting с возможностью единичного и множественного пробуждений На поверхности мы имеем 3 метода:
— Wait — отпустить мьютекс, запарковаться, получить уведомление, распарковаться, взять мьютекс
— Signal — разбудить один поток по FIFO-принципу
— Broadcast — разбудить все потоки Типичный паттерн использования:
cond.L.Lock()
for !condition { cond.Wait() // Ждем cond.Signal() или cond.Broadcast()
}
cond.L.Unlock() // Выполняем полезную работу
// ... Почему мы не выжигаем циклы CPU?
Внутри происходит следующее: горутина отпускает мьютекс, встаёт в очередь ожидания и паркуется. Распарковывается только когда кто-то вызовет Signal() или Broadcast()
Source code 2. Преимущества sync.Cond
На первый взгляд кажется, что каналы решают все задачи координации. Но есть сценарии, где Cond выигрывает: — Broadcast-семантика — нужно разбудить 1000 ожидающих горутин. С каналами это O(n) отправок, и нет гарантии, что проснутся именно все — одна быстрая горутина может выхватить несколько сигналов подряд. Закрытие через close() работает, но канал одноразовый и в этом случае нам придется каким-то подменять канал для перерасчета в select. Broadcast() будит всех, кто спит на момент вызова, каждого ровно один раз, и можно вызывать повторно — Ожидание сложного условия — допустим, поток должен проснуться только когда: буфер не пуст, конфиг перезагружен и количество активных воркеров не превышает лимит
cond.L.Lock()
for len(buf) == 0 || !configReady || activeWorkers >= max { cond.Wait()
}
cond.L.Unlock() — Возможность FIFO-порядка пробуждения — Signal() будит горутины строго в порядке засыпания Звучит идеально? — Почти. Есть одна ловушка, о которой молчит документация... 3. Cond игнорирует context.Context
Cond появился в доисторические времена (Feb, 2012) до появление пакета context как такового (Aug, 2016) и вплоть до версии Go 1.21 не было поддержки реагирования на context.Context внутри метода Wait Взглянем на следующий кейс:
func (q Queue) Get() any { q.cond.L.Lock() defer q.cond.L.Unlock() for len(q.items) == 0 { q.cond.Wait() } return q.pop()
} // Вызывающий код
func handler(ctx context.Context) error { item := queue.Get() // Никак не реагируем на контекст // Если очередь пуста — висим вечно, return process(item)
}
Чем это грозит:
— Горутины утекают и копятся до OOM Killed
— Graceful shutdown не работает — процесс убивается по таймауту
— Клиент ушёл, а сервер продолжает держать ресурсы В качестве решения данной проблемы мы можем использовать Watcher-горутину:
func (q Queue) Get(ctx context.Context) (any, error) { done := make(chan struct{}) // Закрываем канал чтобы убить Watcher-горутину defer close(done) // Watcher: при отмене контекста распарковываем // все ожидающие потоки go func() { select { case <-ctx.Done(): // Гарантируем, что поток(и) запаркован(ы) (отдал мьютекс) q.cond.L.Lock() q.cond.Broadcast() q.cond.L.Unlock() case <-done: } }() q.cond.L.Lock() defer q.cond.L.Unlock() for len(q.items) == 0 { q.cond.Wait() // Проверяем контекст после сигнала select { case <-ctx.Done(): return nil, ctx.Err() default: } } return q.pop(), nil
} Как было подмечено ранее, в версии Go 1.21 в пакете context появилась альтернатива в виде context.AfterFunc (спасибо @isedyh за наводку!) Призываю вас быть бдительными при работе с этой штукой. Не окажитесь в ловушке! Stay tuned 😍 #golang #concurrency