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(), обеспечивая точную синхронизацию независимо от того, сколько времени занимает каждая загрузка.
Попробуйте сами
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