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

При повторном использовании sync.WaitGroup для второй волны задач в какой момент разрешено начинать новый ц...

При повторном использовании sync.WaitGroup для второй волны задач в какой момент разрешено начинать новый цикл Add?

Проходите собеседования с ИИ помощником Hintsage

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

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

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

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

Повторное использование позволяет применять один объект для последовательных волн задач, но требует чёткой границы между ними. Иначе ожидание может относиться не к той группе горутин, которую предполагал разработчик.

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

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

Положительные вызовы Add, переводящие счётчик из нуля в положительное значение, должны произойти до соответствующего Wait. Нарушение этого правила может привести к неверной синхронизации и диагностируемой во время выполнения ошибке.

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

Для первой волны сначала выполняют все положительные вызовы Add, затем запускают горутины. Каждая горутина обязана ровно один раз вызвать Done, обычно через defer. После того как Wait вернулся, первая волна завершена, и объект можно использовать для следующей волны.

package main import "sync" func run(wg *sync.WaitGroup) { wg.Add(1) go func() { defer wg.Done() }() wg.Wait() } func main() { var wg sync.WaitGroup run(&wg) run(&wg) }

В примере второй вызов run начинается только после возврата первого Wait, поэтому повторное использование корректно. Сам WaitGroup нельзя копировать после начала использования; для нескольких функций обычно передают указатель.

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

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

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

Сервис обрабатывает входной пакет в несколько последовательных волн: сначала загружает данные, затем запускает независимые проверки. Вариант с постоянным созданием нового WaitGroup прост и снижает риск ошибочного переиспользования, но требует передавать или создавать отдельный объект для каждой волны.

Вариант с одним общим WaitGroup экономит небольшое количество объектов, но требует строгого правила: новый Add начинается только после завершения предыдущего Wait. Выбран вариант с одним объектом и последовательным протоколом, потому что волны не пересекаются; после каждой волны вызывается Wait, а затем начинается следующая. Это сохраняет понятную границу жизненного цикла и исключает гонку перехода счётчика через ноль.

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

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

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

  1. Что произойдёт, если одна горутина дважды вызовет Done?

Счётчик уменьшится лишний раз. Если число вызовов Done превысит число положительных приращений, WaitGroup обнаружит отрицательный счётчик и вызовет панику. Поэтому соответствие «один успешно учтённый запуск — один Done» является обязательным.

  1. Достаточно ли дождаться возврата Wait, чтобы безопасно читать результаты горутин?

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