Это может стать вашим ускоренным курсом по WaitGroup. Давайте начнем шаг за шагом.
Прочитайте до конца, иначе вы можете узнать что-то неправильное и обвинить в этом меня😝
0. Пример рабочего
Ниже приведен простой пример с рабочей функцией для печати сообщения. Нет параллелизма 🍰.
// https://go.dev/play/p/vWL9zhQC1LT package main import ( "fmt" ) func worker(msg string) { fmt.Println("Worker", msg) } func main() { worker("w1") fmt.Println("exiting") }
Вывод:
Worker w1 exiting
1. Больше рабочих 🐜🐜🐜
// https://go.dev/play/p/qE-UT_NM5Q3 package main import ( "fmt" ) func worker(msg string) { fmt.Println("Worker", msg) } func main() { worker("w1") worker("w2") worker("w3") fmt.Println("exiting") }
Вывод:
Worker w1 Worker w2 Worker w3 exiting
2. Добавьте параллелизма 🧂
// https://go.dev/play/p/qE-UT_NM5Q3 package main import ( "fmt" ) func worker(msg string) { fmt.Println("Worker", msg) } func main() { go worker("w1") go worker("w2") go worker("w3") fmt.Println("exiting") }
Вывод:
Worker w1 Worker w2 Worker w3 exiting
или, может быть
Worker w1 exiting
or
Worker w1 Worker w2 exiting
или даже
Worker w2 Worker w1 exiting
Честно говоря, мы не знаем 🤷🏻♂️
3. Исправление с помощью группы ожидания 🔧
Чтобы убедиться, что мы ждем завершения всех рабочих. У WaitGroup есть простой API.
- Add(int) добавляет дельту, которая может быть отрицательной, к счетчику группы ожидания.
- Done() уменьшает значение счетчика группы ожидания на единицу.
- Wait() блокируется до тех пор, пока счетчик WaitGroup не станет равным нулю.
// https://go.dev/play/p/r-4JT1upJQf package main import ( "fmt" "sync" ) var wg sync.WaitGroup func worker(msg string) { wg.Add(1) defer wg.Done() fmt.Println("Worker", msg) } func main() { go worker("w1") go worker("w2") go worker("w3") fmt.Println("waiting") wg.Wait() fmt.Println("exiting") }
Вывод:
waiting Worker w1 Worker w2 Worker w3 exiting
or
Worker w1 waiting Worker w2 Worker w3 exiting
or
Worker w2 waiting Worker w1 Worker w3 exiting
или что-то другое. Но у нас есть гарантия. Гарантируйте, что exiting
всегда печатается после того, как все строки, начинающиеся с Worker
, будут напечатаны.
4. Передача группы ожидания в качестве аргумента
Часто... или в основном мы не создаем глобальные переменные. Мы создаем переменные области видимости функции. Давайте посмотрим, как изменится наш код, когда мы передаем функцию WaitGroup.
// https://go.dev/play/p/QoCJIbPkXkC package main import ( "fmt" "sync" ) func worker(msg string, wg sync.WaitGroup) { wg.Add(1) defer wg.Done() fmt.Println("Worker", msg) } func main() { var wg sync.WaitGroup go worker("w1", wg) go worker("w2", wg) go worker("w3", wg) fmt.Println("waiting") wg.Wait() fmt.Println("exiting") }
Обратите внимание, что объявление wg перемещается внутри main, и наш рабочий процесс теперь принимает группу ожидания в качестве второго аргумента.
Вывод:
waiting exiting
Не совсем ожидаемо, верно !!
Причина: Если вы внимательно изучите определение WaitGroup
методов, вы увидите приемник указателя. И WaitGroup — это не интерфейс с реализацией, использующей приемник указателей💡.
Это означает, что если мы передаем группу ожидания без указателя, создается копия.
Также обратите внимание
5. Исправим 💪🏻
// https://go.dev/play/p/QoCJIbPkXkC package main import ( "fmt" "sync" ) // Notice the * here func worker(msg string, wg *sync.WaitGroup) { wg.Add(1) defer wg.Done() fmt.Println("Worker", msg) } func main() { var wg sync.WaitGroup go worker("w1", &wg) go worker("w2", &wg) go worker("w3", &wg) fmt.Println("waiting") wg.Wait() fmt.Println("exiting") }
Приготовьтесь 🎇🎉🎆🎉🎇
Вывод:
waiting Worker w1 Worker w2 Worker w3 exiting
Вы действительно получаете выше вывода ?? Эх... одураченный параллелизмом.
Запустите его достаточно раз, и вы получите это
waiting exiting
Или что-то еще странное.
Но подождите, почему.. в чем проблема 😵
6. Просветление 🧠
Проблема не в группе ожидания или ссылке. Проблема в заказе.
Обратите внимание, что две инструкции wg.Add(1)
и defer wg.Done()
написаны внутри процедуры go, а не в вызывающей программе. Таким образом, возможно, что вызывающая сторона запускает подпрограмму go... на самом деле все подпрограммы go, но ни одна из них не начинает выполнение, пока мы не достигнем строки wg.Wait()
. Все процедуры go ожидают выполнения своего первого оператора wg.Add(1)
, в то время как основной поток продолжается до конца.
Ждать !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
Как, черт возьми, наш третий пример «2. Добавьте немного параллелизма 🧂» тогда работайте 🙄
Чтобы ответить на этот вопрос самостоятельно, запустите его пару раз, пока не произойдет волшебство. И наш третий пример логически неверный и не гарантирует выполнение всех воркеров.
Как исправить?
Это обычная конструкция, которую вы найдете в коде go. Что выглядит примерно так
// https://go.dev/play/p/QoCJIbPkXkC package main import ( "fmt" "sync" ) func worker(msg string) { fmt.Println("Worker", msg) } func main() { var wg sync.WaitGroup wg.Add(1) go func() { defer wg.Done() worker("w1") }() wg.Add(1) go func() { defer wg.Done() worker("w2") }() wg.Add(1) go func() { defer wg.Done() worker("w3") }() fmt.Println("waiting") wg.Wait() fmt.Println("exiting") }
Приготовьтесь 🎇🎉🎆🎉🎇
Вывод:
waiting Worker w1 Worker w2 Worker w3 exiting
Обратите внимание на тему
wg.Add(1) go func() { defer wg.Done() worker("w1") }()
wg.Add(1)
появляется перед входом в программу go
defer wg.Done()
— это первый оператор в процедуре go.
Эти две вещи обеспечивают правильное завершение наших одновременных рабочих процессов.
Также обратите внимание на еще пару важных вещей:
- нам больше не нужно передавать WaitGroup рабочему процессу
- вызывающий объект является владельцем
wg
и заботится об очистке - worker содержит только базовую логику и не должен иметь дело с какими-либо конструкциями параллелизма
Фу 😅
Это большая часть.
7. Можем ли мы сделать лучше?
Могут быть разные способы выражения вышеприведенного кода с использованием циклов. Так мы это видим и на практике. Ниже приведен пример использования цикла для порождения трех рабочих.
// https://go.dev/play/p/0UiybqXphC0 package main import ( "fmt" "sync" ) func worker(msg string) { fmt.Println("Worker", msg) } func main() { var wg sync.WaitGroup for i := 1; i <= 3; i++ { wg.Add(1) go func(seq int) { defer wg.Done() worker(fmt.Sprintf("w%d", seq)) }(i) } fmt.Println("waiting") wg.Wait() fmt.Println("exiting") }
Ключевые выводы
- Избегайте передачи WaitGroup в качестве аргумента
- Если нужно передать WaitGroup в качестве аргумента, сделайте это по ссылке
- Используйте общий синтаксис использования группы ожидания.
wg.Add(1) // 1 - Add go func() { // 2 - The go routine defer wg.Done() // 3 - Done someFunc() // 4 - Your Code } () // 5 - Remember to call the goroutine
Параллелизм настолько сложен, что я до сих пор не уверен, объяснил ли я все приведенные выше примеры достаточно подробно и правильно. Мы все делаем ошибки, и они помогают нам учиться. Развлекайтесь и ИЗБЕГАЙТЕ использования параллельного кода в рабочей среде, пока это не станет действительно необходимым.