Можно ли динамически изменять счетчик `sync.WaitGroup`?

Ответ

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

Основные методы для этого — Add(delta int) для увеличения (или уменьшения) счетчика и Done() для его уменьшения на единицу.

Пример динамического добавления задач:

package main

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

func main() {
    var wg sync.WaitGroup

    // Запускаем 3 начальные горутины
    wg.Add(3)
    for i := 1; i <= 3; i++ {
        go func(id int) {
            defer wg.Done()
            fmt.Printf("Задача %d выполняетсяn", id)
            time.Sleep(100 * time.Millisecond)
        }(i)
    }

    // В процессе выполнения решаем добавить еще 2 задачи
    fmt.Println("Динамически добавляем еще 2 задачи...")
    wg.Add(2)
    for i := 4; i <= 5; i++ {
        go func(id int) {
            defer wg.Done()
            fmt.Printf("Дополнительная задача %d выполняетсяn", id)
            time.Sleep(200 * time.Millisecond)
        }(i)
    }

    wg.Wait() // Ожидаем завершения всех 5 задач
    fmt.Println("Все задачи выполнены.")
}

Важные правила и риски:

  1. Состояние гонки (Race Condition): Вызывать Add() нужно до того, как счетчик может стать равен нулю. Если wg.Wait() сработает до того, как другая горутина вызовет wg.Add(), программа завершится преждевременно. Лучшая практика — вызывать Add() в той же горутине, которая запускает новые дочерние горутины, и делать это до их запуска.
  2. Паника при отрицательном счетчике: Если вызов Done() или Add() с отрицательным значением сделает внутренний счетчик отрицательным, программа запаникует (panic: sync: negative WaitGroup counter).
  3. Паника после Wait(): Повторное использование WaitGroup после того, как Wait() уже отработал (т.е. счетчик стал 0), небезопасно. Если нужно повторить цикл ожидания, создайте новый экземпляр WaitGroup.

Ответ 18+ 🔞

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

Смотри, основные приколы — это Add(delta int), чтобы счетчик туда-сюда двигать, и Done(), который просто на единичку убавляет.

Вот, смотри, как можно по ходу пьесы задачи подкидывать:

package main

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

func main() {
    var wg sync.WaitGroup

    // Запускаем 3 начальные горутины
    wg.Add(3)
    for i := 1; i <= 3; i++ {
        go func(id int) {
            defer wg.Done()
            fmt.Printf("Задача %d выполняетсяn", id)
            time.Sleep(100 * time.Millisecond)
        }(i)
    }

    // В процессе выполнения решаем добавить еще 2 задачи
    fmt.Println("Динамически добавляем еще 2 задачи...")
    wg.Add(2)
    for i := 4; i <= 5; i++ {
        go func(id int) {
            defer wg.Done()
            fmt.Printf("Дополнительная задача %d выполняетсяn", id)
            time.Sleep(200 * time.Millisecond)
        }(i)
    }

    wg.Wait() // Ожидаем завершения всех 5 задач
    fmt.Println("Все задачи выполнены.")
}

А теперь, сука, слушай сюда, правила, которые нарушать — себе дороже:

  1. Гонка, блядь (Race Condition): Ты должен успеть крикнуть Add() до того, как счетчик упадет в ноль. Представь: wg.Wait() уже отработал, программа думает, что всё, пиздец, можно выходить, а ты тут со своим wg.Add() из другой горутины. И всё, прощай, выполнение, ушли по-английски, не дождавшись. Идеальный вариант — вызывай Add() в той же горутине, которая новых работяг запускает, и ДО их запуска, ёпта!
  2. Паника от отрицательного счетчика: Ну тут всё просто, блядь. Если ты вызовешь Done() или Add() с минусом так, что счетчик уйдет в отрицатели — получишь в лоб panic: sync: negative WaitGroup counter. И правильно, идиот, сам виноват.
  3. Паника после Wait(): Вот отработал твой WaitGroup, счетчик ноль. Всё, пизда, приехали. Он одноразовый, понимаешь? Нельзя его, блядь, как старую перчатку, использовать повторно. Хочешь новый цикл — создавай новый экземпляр, и никаких геморроев.