sync.WaitGroup
Часть раздела Объектно-ориентированное программирование путешествия по GO на Coddy — урок 64 из 107.
В уроке «Основы Goroutines» мы использовали time.Sleep, чтобы дождаться завершения выполнения горутин — это ненадежный подход. sync.WaitGroup предоставляет правильный способ ожидания завершения работы группы горутин.
WaitGroup работает как счетчик. Вы увеличиваете его перед запуском горутины, уменьшаете, когда горутина завершается, и блокируете выполнение до тех пор, пока счетчик не достигнет нуля:
func main() {
var wg sync.WaitGroup
for i := 1; i <= 3; i++ {
wg.Add(1) // инкрементируем счетчик
go func(id int) {
defer wg.Done() // декрементируем по завершении
fmt.Printf("Worker %d finished\n", id)
}(i)
}
wg.Wait() // ожидаем, пока счетчик не станет равен 0
fmt.Println("All workers complete")
}Три основных метода — это Add(n) для увеличения счетчика на n, Done() для его уменьшения на единицу (эквивалентно Add(-1)), и Wait() для блокировки до тех пор, пока счетчик не достигнет нуля. Использование defer wg.Done() гарантирует уменьшение счетчика, даже если горутина завершается с паникой.
Распространенная ошибка — вызов Add внутри горутины вместо того, чтобы сделать это перед ней. Это создает состояние гонки (race condition), при котором Wait может завершиться до того, как все горутины будут зарегистрированы:
// Неправильно — состояние гонки
go func() {
wg.Add(1) // может выполниться после Wait()
defer wg.Done()
}()
// Правильно — вызывайте Add перед запуском
wg.Add(1)
go func() {
defer wg.Done()
}()WaitGroups часто встраиваются в структуры для координации параллельных операций, что делает их необходимыми для создания потокобезопасных типов в Go.
Задание
ЛегкоДавайте создадим менеджер загрузок, который координирует несколько одновременных загрузок файлов с помощью sync.WaitGroup. Ваш менеджер будет отслеживать завершение всех загрузок, не полагаясь на произвольные таймеры ожидания.
Вы организуете свой код в двух файлах:
downloader.go: Определите логику координации загрузки.Создайте структуру
Downloadс полямиID(int),Filename(string) иSize(int, представляющий КБ).Реализуйте метод
Process(wg *sync.WaitGroup)дляDownload, который имитирует загрузку путем приостановки выполнения на время, зависящее от размера файла (используйтеSize * 10миллисекунд). Когда загрузка завершится, выведите:Downloaded: [Filename] ([Size]KB). Используйтеdefer wg.Done(), чтобы гарантировать правильное уменьшение счетчика WaitGroup.Создайте функцию
StartDownloads(downloads []Download) int, которая запускает все загрузки одновременно с использованием горутин и ожидает их завершения. Не забудьте вызватьwg.Add(1)перед запуском каждой горутины, а не внутри нее. Верните общий размер всех загрузок вместе взятых.main.go: Считайте информацию о загрузках и организуйте параллельные загрузки.Считайте количество загрузок, затем для каждой загрузки считайте ее ID, имя файла и размер. Создайте объекты загрузок и передайте их в
StartDownloads. После завершения всех загрузок выведите:All downloads complete: [total]KB total
Будут предоставлены следующие входные данные:
- Строка 1: Количество загрузок (целое число)
- Следующие строки: Для каждой загрузки три строки — ID (целое число), имя файла (строка) и размер в КБ (целое число)
Например, при вводе:
3
1
report.pdf
20
2
image.png
5
3
data.csv
10Ваш вывод должен показывать завершение загрузок (файлы меньшего размера завершаются первыми из-за более короткого времени ожидания), за которым следует сводка:
Downloaded: image.png (5KB)
Downloaded: data.csv (10KB)
Downloaded: report.pdf (20KB)
All downloads complete: 35KB totalКлючевое отличие от использования time.Sleep заключается в том, что wg.Wait() блокирует выполнение до тех пор, пока все горутины не просигнализируют о завершении с помощью Done(), обеспечивая точную синхронизацию независимо от того, сколько времени занимает каждая загрузка.
Шпаргалка
sync.WaitGroup предоставляет правильный способ ожидания завершения горутин. Он работает как счетчик, который отслеживает запущенные горутины.
Три основных метода:
Add(n)— увеличивает счетчик на nDone()— уменьшает счетчик на единицу (эквивалентноAdd(-1))Wait()— блокирует выполнение до тех пор, пока счетчик не станет равным нулю
Базовый паттерн использования:
var wg sync.WaitGroup
for i := 1; i <= 3; i++ {
wg.Add(1) // инкремент перед запуском горутины
go func(id int) {
defer wg.Done() // декремент по завершении
fmt.Printf("Worker %d finished\n", id)
}(i)
}
wg.Wait() // блокировка до завершения всех горутин
fmt.Println("All workers complete")Использование defer wg.Done() гарантирует, что счетчик уменьшится, даже если в горутине произойдет паника.
Важно: Всегда вызывайте Add перед запуском горутины, а не внутри нее, чтобы избежать состояния гонки (race conditions):
// Неправильно — состояние гонки
go func() {
wg.Add(1) // может выполниться после Wait()
defer wg.Done()
}()
// Правильно
wg.Add(1)
go func() {
defer wg.Done()
}()Попробуйте сами
package main
import (
"fmt"
)
func main() {
// Считать количество загрузок
var n int
fmt.Scanln(&n)
// Считать информацию о загрузках
downloads := make([]Download, n)
for i := 0; i < n; i++ {
var id int
var filename string
var size int
fmt.Scanln(&id)
fmt.Scanln(&filename)
fmt.Scanln(&size)
downloads[i] = Download{
ID: id,
Filename: filename,
Size: size,
}
}
// TODO: Вызвать StartDownloads со срезом downloads
// TODO: Вывести сводку: "All downloads complete: [total]KB total"
}
В этом уроке есть небольшой тест. Начните урок, чтобы ответить на вопросы и сохранить прогресс.
Все уроки раздела Объектно-ориентированное программирование
1Основы ООП в Go
Внешние файлыРабочее пространство и модули GoПакеты и импортыЭкспортируемые и неэкспортируемые именаВведение в ООП в GoСтруктуры как классыОпределение методов структурПолучатели-указатели и получатели-значенияИнициализация структурФункции-конструкторыИтоги — Простой калькулятор4Интерфейсы
Введение в интерфейсыНеявная реализацияИнтерфейс как контрактПустой интерфейс (any)Утверждение типаПереключатель типовКомпозиция интерфейсовИнтерфейсы Stringer и ErrorПовторение: Калькулятор фигур7Инкапсуляция
Экспортируемые и неэкспортируемые поляИнкапсуляция на уровне пакетовГеттеры и сеттерыСокрытие информации в GoИтоги — Записи о студентах10Обобщения (Generics) (Go 1.18+)
Введение в GenericsПараметры типовОграничения типовОбобщенные структурыОбходной путь для обобщенных методовИтоги — Обобщенная коллекция13Паттерны проектирования. Часть 1
Введение в паттерны проектированияПаттерн SingletonПаттерн FactoryПаттерн Abstract FactoryПаттерн ObserverПаттерн Strategy2Глубокое погружение в типы и структуры
Базовые и составные типыОпределение пользовательских типовТеги структурАнонимные структурыВложенные структурыНулевые значения и значения по умолчаниюПовторение — Контактная книга5Композиция вместо наследования
Почему в Go нет наследованияОсновы встраивания структурПродвижение методовВстраивание нескольких структурВстраивание против агрегацииЗатенение встроенных методовИтоги — Иерархия сотрудников8Обработка ошибок и ООП
Интерфейс errorПользовательские типы ошибокОбертывание ошибок (fmt.Errorf)Sentinel-ошибкиerrors.Is() и errors.As()Panic, Defer и RecoverИтоги — Парсер файлов11Стандартная библиотека и ООП
io.Reader и io.Writersort.InterfaceИнтерфейс fmt.Stringerencoding/json со структурамиИнтерфейс http.HandlerПовторение: модели REST API14Паттерны проектирования. Часть 2
Паттерн КомандаПаттерн АдаптерПаттерн ДекораторПаттерн Шаблонный методПаттерн СостояниеПаттерн КомпоновщикMiddleware как Декоратор3Указатели и память
Основы указателей в GoУказатели на структурыПередача по значению и по ссылкеФункция new()Сборка мусора в GoПовторение: Конструктор связного списка6Полиморфизм в Go
Полиморфизм через интерфейсыУтиная типизация в GoПравила реализации интерфейсовПолиморфные коллекцииВнедрение зависимостейИтоги — Обработчик платежей9Конкурентность и ООП
Основы горутинКаналы и взаимодействиеБуферизованные и небуферизованные каналыОператор selectsync.Mutex и sync.RWMutexsync.WaitGroupПроектирование потокобезопасных структурПовторение — Worker Pool