Добро пожаловать в раздел Введение в параллелизм в Go! В прошлый раз мы впервые попробовали параллелизм в Go. Мы рассмотрели, что такое горутины и каналы, различия между буферизованными и небуферизованными каналами и простой способ дождаться завершения горутин. На этот раз мы поговорим о WaitGroups, другом способе синхронизации горутин.

Что такое группы ожидания?

WaitGroups — это эффективный способ синхронизации горутин. Помните, как мы использовали каналы для ожидания завершения других горутин? WaitGroups может помочь вам достичь той же цели. Представьте себе автомобильную поездку с семьей. Твой папа заходит в торговый центр или ресторан быстрого питания, чтобы перекусить и сходить в туалет. В идеале вы хотите дождаться, пока все вернутся, прежде чем снова уехать за горизонт. Группы ожидания помогут вам в этом.

Группы ожидания определяются путем вызова пакета sync из стандартной библиотеки Go.

var wg sync.WaitGroup

Так что же такое WaitGroup? Группа ожидания — это структура, содержащая определенную информацию о том, сколько горутин программа должна ожидать. Это группа, содержащая количество горутин, которые вам нужно ждать.

Группы ожидания имеют три наиболее важных метода: Add, Done и Wait.

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

Как использовать группы ожидания

Давайте посмотрим пример фрагмента кода.

package main
import (
    "fmt"
    "sync"
    "time"
)
func main() {
    var wg sync.WaitGroup
    wg.Add(1)
    go func() {
        defer wg.Done()
        fmt.Println(time.Now(), "start")
        time.Sleep(time.Second)
        fmt.Println(time.Now(), "done")
    }()
    wg.Wait()
    fmt.Println(time.Now(), "exiting...")
}
2022-08-21 17:01:54.184744229 +0900 KST m=+0.000021800 start
2022-08-21 17:01:55.184932851 +0900 KST m=+1.000210473 done
2022-08-21 17:01:55.18507731 +0900 KST m=+1.000354912 exiting...
  • Сначала мы инициализируем экземпляр группы ожидания wg.
  • Затем мы добавляем 1 к wg, потому что хотим дождаться завершения одной горутины.
  • Затем мы запускаем горутину. Внутри горутины мы делаем defer вызов wg.Done(), чтобы убедиться, что мы уменьшили количество горутин для ожидания. Если мы этого не сделаем, код будет вечно ждать завершения горутины, что приведет к взаимоблокировке.
  • После вызова горутины мы обязательно блокируем код до тех пор, пока группа ожидания не станет пустой. Мы делаем это, вызывая wg.Wait().

Зачем использовать группы ожидания вместо каналов?

Теперь, когда мы знаем, как использовать группы ожидания, естественный ход мыслей приводит нас к следующему вопросу: зачем использовать группы ожидания по каналам?

Исходя из моего опыта, причин несколько.

  • Группы ожидания более интуитивно понятны. Когда вы читаете фрагмент кода, когда видите группу ожидания, вы сразу понимаете, что делает этот код. Имена методов являются явными и точными. Однако с каналами иногда не все так однозначно. Использование каналов разумно, но может быть сложно понять, когда вы читаете сложный фрагмент кода.
  • Бывают случаи, когда вам не нужно использовать канал. Например, давайте посмотрим на этот код:
var wg sync.WaitGroup
for i := 0; i < 5; i++ {     
    wg.Add(1)
    go func() {         
        defer wg.Done()
        fmt.Println(time.Now(), "start")
        time.Sleep(time.Second)
        fmt.Println(time.Now(), "done")
    }()
}
wg.Wait()
fmt.Println(time.Now(), "exiting...")
  • Вы можете видеть, что горутина не передает данные другим горутинам. Если ваши горутины — это одноразовые задания, результаты которых вам не нужны, желательно использовать группу ожидания. Теперь взгляните на этот код:
ch := make(chan int)
for i := 0; i < 5; i++ {     
    go func() {         
        randomInt := rand.Intn(10)         
        ch <- randomInt     
    }() 
}
for i := 0; i < 5; i++ {     
    fmt.Println(<-ch) 
}
  • Здесь горутина отправляет данные обратно в канал. В этих случаях нам не нужно использовать группу ожидания, потому что это было бы избыточно. Зачем ждать завершения горутин, если получение уже достаточно блокирует?

Группы ожидания предназначены для работы с ожидающими горутинами. Я чувствую, что основная цель канала — передавать данные. Вы не можете использовать группу ожидания для отправки и получения данных, но вы можете использовать канал для синхронизации своих горутин.

В конце концов, правильного ответа нет. Я знаю, что это может раздражать, но решать вам и команде, с которой вы работаете. Что бы ни работало, это лучше всего, и ни один ответ не является неправильным. Лично я предпочитаю использовать для синхронизации WaitGroups, но ваш вариант может отличаться. Выберите что-то наиболее интуитивное для вас.

Одна вещь, на которую следует обратить внимание

Иногда вам может понадобиться передать экземпляр WaitGroup горутине. Может быть несколько групп ожидания, которые обрабатывают разные горутины, или, может быть, это выбор дизайна. Какой бы ни была причина, обязательно передайте указатель на группу ожидания, например так:

var wg sync.WaitGroup
for i := 0; i < 5; i++ {
    wg.Add(1)
    go func(wg *sync.WaitGroup) {
        defer wg.Done()
        fmt.Println(time.Now(), "start")
        time.Sleep(time.Second)
        fmt.Println(time.Now(), "done")
    }(&wg)
}
wg.Wait()
fmt.Println(time.Now(), "exiting...")

Причина в том, что Go — это язык передачи по значению. Это означает, что всякий раз, когда вы передаете аргумент функции, Go создает копию аргумента и передает ее вместо исходного объекта. Что происходит в этом контексте, так это то, что весь объект WaitGroup будет скопирован, а это означает, что горутина будет иметь дело с совершенно другой группой ожидания. wg.Done() не будет вычитаться из исходного wg, а будет его копией, которая живет только внутри горутины.

Заключение

Извините за загрузку позже обычного. Я был занят подготовкой к школе, и по прибытии в Штаты мне нужно было выполнить некоторые первоначальные настройки. Я должен был что-то сказать об этом, но забыл. Я обязательно буду держать вас в курсе, ребята, с этого момента!

Я надеюсь, что это руководство помогло вам легко разработать параллельный код! Группы ожидания просты для понимания и реализации. В следующий раз мы поговорим о том, как слушать несколько каналов одновременно. Увидимся на следующей неделе!

Вы также можете прочитать этот пост на Dev.to и моем личном сайте.