Menu
Coddy logo textTech

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.

challenge icon

Задание

Легко

Давайте создадим менеджер загрузок, который координирует несколько одновременных загрузок файлов с помощью 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) — увеличивает счетчик на n
  • Done() — уменьшает счетчик на единицу (эквивалентно 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"
}
quiz iconПроверьте себя

В этом уроке есть небольшой тест. Начните урок, чтобы ответить на вопросы и сохранить прогресс.

Все уроки раздела Объектно-ориентированное программирование