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

Попробуйте сами

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Проверьте себя

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

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