G
go-with-me
@golangwithus1.9K подп.
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
4.0K
просмотров
3919
символов
Да
эмодзи
Да
медиа

Другие посты @golangwithus

Все посты канала →
💥sync package. Cond's hidden trap В этот раз поговорим про — @golangwithus | PostSniper