Для чего используется `sync.WaitGroup` и каковы правила работы с ним?

Ответ

sync.WaitGroup — это механизм синхронизации, который позволяет основной горутине ожидать завершения выполнения набора других (дочерних) горутин.

Основные методы:

  1. Add(delta int) Увеличивает внутренний счетчик активных горутин на delta. Важно вызывать этот метод в основной горутине до запуска дочерней горутины, чтобы избежать состояния гонки.

  2. Done() Уменьшает счетчик на единицу. Обычно вызывается в конце работы горутины с помощью defer, чтобы гарантировать выполнение даже в случае паники внутри горутины.

  3. Wait() Блокирует горутину, в которой был вызван, до тех пор, пока внутренний счетчик не станет равен нулю.

Пример использования:

package main

import (
    "fmt"
    "sync"
    "time"
)

func worker(id int, wg *sync.WaitGroup) {
    defer wg.Done() // Уменьшаем счетчик при выходе из функции

    fmt.Printf("Worker %d startingn", id)
    time.Sleep(time.Second)
    fmt.Printf("Worker %d donen", id)
}

func main() {
    var wg sync.WaitGroup

    for i := 1; i <= 3; i++ {
        wg.Add(1) // Увеличиваем счетчик перед запуском горутины
        go worker(i, &wg)
    }

    fmt.Println("Main: Waiting for workers to finish...")
    wg.Wait() // Ожидаем, пока счетчик не станет 0
    fmt.Println("Main: All workers finished. Exiting.")
}

Ключевые правила и частые ошибки:

  • Вызов Add до запуска горутины: Если вызвать wg.Add(1) внутри самой горутины, wg.Wait() может сработать раньше, чем Add выполнится, что приведет к преждевременному завершению программы.
  • Отрицательный счетчик: Количество вызовов Done() не должно превышать количество, указанное в Add. Это вызовет панику panic: sync: negative WaitGroup counter.
  • Копирование WaitGroup: WaitGroup нельзя копировать после первого использования. Ее следует передавать по указателю (*sync.WaitGroup).
  • Переиспользование: После того как Wait() отработал и счетчик обнулился, WaitGroup можно использовать заново для новой группы горутин, снова вызвав Add.

Ответ 18+ 🔞

Слушай, а вот этот твой sync.WaitGroup — это ж такая штука, чтобы главная горутина не сбежала раньше времени, пока все её мелкие подчинённые не отработают. Как мамка у подъезда ждёт, пока все дети с прогулки не приползут, блядь.

Короче, три его главных прикола:

  1. Add(delta int) — это типа крикнуть: «Так, я сейчас трёх хулиганов на улицу выпускаю, ждите!». Увеличиваешь счётчик на delta. Делать это надо до того, как горутину запустил, а то она уже убежала, а ты ещё считаешь — пиздец, недосчитаешься.
  2. Done() — это когда один хулиган пришёл домой и дверь за собой закрыл. Уменьшает счётчик на единичку. Обычно его через defer ставят, чтобы даже если горутина обосрётся с паникой, она всё равно «отметилась», что пришла. Ответственность, блядь.
  3. Wait() — это собственно мамка, которая стоит и курит, пока счётчик не станет ноль. Блокируется, короче, до победного.

Вот смотри, как это в деле выглядит, ёпта:

package main

import (
    "fmt"
    "sync"
    "time"
)

func worker(id int, wg *sync.WaitGroup) {
    defer wg.Done() // Что бы ни случилось — я вернулся, чёрт возьми!

    fmt.Printf("Рабочий %d начал страдатьn", id)
    time.Sleep(time.Second) // Делаем вид, что пашем
    fmt.Printf("Рабочий %d закончил страдатьn", id)
}

func main() {
    var wg sync.WaitGroup

    for i := 1; i <= 3; i++ {
        wg.Add(1) // Внимание, ещё один страдалец на выход!
        go worker(i, &wg)
    }

    fmt.Println("Главный: Сижу, жду, когда все настрадаются...")
    wg.Wait() // Сижу тут, блядь, пока все трое не отчитаются
    fmt.Println("Главный: Всё, все выдохлись. Можно идти.")
}

А теперь главное — где все обычно ебутся:

  • Add в неположенном месте. Если wg.Add(1) запихнуть внутрь горутины, то мамка (Wait()) может уже «всё, я пошла» сказать, пока ты там в другой комнате только собираешься счётчик увеличить. Раньше времени разблокируется — и пиши пропало.
  • Перебор с Done. Вызвал Done() лишний раз — получи panic: sync: negative WaitGroup counter. Это как если бы ты сказал, что домой вернулось пятеро, а выпускал-то ты только троих. Пиздец, откуда двое взялись? Математика, сука!
  • Попытка скопировать. WaitGroup — как зубная щётка, индивидуальная. После того как начал её использовать — не копируй, а передавай дальше указателем (&wg), а то сломаешь.
  • Можно использовать повторно. Всё обнулилось после Wait()? Ну и хуй с ним, можно снова Add вызывать и новую партию рабов запускать. Многоразовая штука.

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