Что такое семафор в Go, для чего он нужен и чем отличается от Mutex и Worker Pool?

Ответ

Семафор — это примитив синхронизации, который позволяет ограничить количество горутин, одновременно выполняющих определенный участок кода или получающих доступ к определенному ресурсу. Его можно представить как счетчик, который отслеживает количество свободных "слотов".

В Go стандартная реализация семафора находится в пакете golang.org/x/sync/semaphore.

Основное назначение:

Контроль параллелизма (concurrency) для предотвращения перегрузки внешних систем (например, API с ограничением по частоте запросов, база данных с лимитом соединений) или для ограничения потребления ресурсов (памяти, CPU).

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

Допустим, нам нужно обработать 100 задач, но мы не хотим нагружать систему и хотим выполнять не более 5 задач одновременно.

package main

import (
    "context"
    "fmt"
    "sync"
    "time"

    "golang.org/x/sync/semaphore"
)

func main() {
    // Создаем семафор с "весом" 5, т.е. не более 5 горутин одновременно
    sem := semaphore.NewWeighted(5)
    ctx := context.Background()
    var wg sync.WaitGroup

    for i := 0; i < 20; i++ {
        wg.Add(1)
        go func(id int) {
            defer wg.Done()

            // Пытаемся "захватить" один слот. Если все заняты, горутина блокируется.
            if err := sem.Acquire(ctx, 1); err != nil {
                fmt.Printf("Failed to acquire semaphore: %vn", err)
                return
            }
            // По окончании работы "освобождаем" слот
            defer sem.Release(1)

            fmt.Printf("Задача %d: выполняется работа...n", id)
            time.Sleep(1 * time.Second)
            fmt.Printf("Задача %d: работа завершена.n", id)
        }(i)
    }

    wg.Wait()
}

Сравнение с другими примитивами:

  • Семафор vs. Mutex:

    • Mutex — это, по сути, бинарный семафор (с весом 1). Он предоставляет эксклюзивный доступ к ресурсу (либо занято, либо свободно).
    • Семафор позволяет получить доступ к ресурсу N горутинам одновременно.
  • Семафор vs. Worker Pool:

    • Worker Pool — это заранее созданный пул из N горутин, которые ожидают задачи из канала. Количество горутин фиксировано.
    • Семафор не создает горутины. Он лишь ограничивает количество уже запущенных горутин, которые могут войти в критическую секцию. Это более гибкий подход, если горутины создаются динамически для выполнения короткоживущих задач.

Ответ 18+ 🔞

А, семафор, говоришь? Ну это ж классика, блядь! Представь себе, сука, очередь в сортир на каком-нибудь концерте, ёпта. Всего три кабинки, а желающих обоссаться — дохуя. Вот семафор — это такой мужик с палкой на входе, который пускает внутрь только троих, а остальные, блядь, ждут, пока кто-то выйдет и освободит место. Всё просто, как три копейки!

В Гошке, правда, стандартного такого мужика в коробке нет, надо брать со стороны, из golang.org/x/sync/semaphore. Но он норм, работает.

Зачем это, нахуй, нужно? Ну, например, ты лезешь в какую-нибудь апишку, а она тебе: «Не больше пяти запросов в секунду, а то по ебалу накостыляю!». Или база данных: «Больше десяти коннектов одновременно — я сяду, и вы все сядете!». Вот тут-то наш мужик с палкой и пригодится. Он не даст всем твоим горутинам-распиздяям одновременно вломиться куда не надо.

Смотри, как это выглядит в коде:

Допустим, у тебя двадцать задач, но одновременно ты хочешь, чтобы в работе было не больше пяти. Остальные пусть постоят, подумают о жизни.

package main

import (
    "context"
    "fmt"
    "sync"
    "time"

    "golang.org/x/sync/semaphore"
)

func main() {
    // Вот он, наш вышибала. Вес 5 — значит, пускает пятерых.
    sem := semaphore.NewWeighted(5)
    ctx := context.Background()
    var wg sync.WaitGroup

    for i := 0; i < 20; i++ {
        wg.Add(1)
        go func(id int) {
            defer wg.Done()

            // Пытаемся пройти. Acquire — это типа «эй, мужик, можно я?»
            // Если мест нет — стой и не дыши, пока кто-то не выйдет.
            if err := sem.Acquire(ctx, 1); err != nil {
                fmt.Printf("Не пустил, сука: %vn", err)
                return
            }
            // Выходя — ОБЯЗАТЕЛЬНО крикни «Release!», а то другие так и будут ждать.
            defer sem.Release(1)

            fmt.Printf("Задача %d: начинаю ебашить...n", id)
            time.Sleep(1 * time.Second) // Делаем вид, что работаем
            fmt.Printf("Задача %d: всё, готово, пиздец.n", id)
        }(i)
    }

    wg.Wait() // Ждём, пока все горутины закончат свои дела
}

Чем он не является, блядь?

  • Семафор vs Мьютекс: Мьютекс — это как сортир на одного. Или занято, или свободно. А семафор — это именно на несколько дырок. Если семафор с весом 1 — да, это почти мьютекс, но, блядь, с нюансами.
  • Семафор vs Воркер-пул: Воркер-пул — это когда ты заранее нанял пятерых работяг (горутин) и кидаешь им задачи в канал. А семафор — это когда горутины сами понабежали, как тараканы, а ты им просто не даёшь всем сразу в одну миску лезть. Гибче, блядь, в некоторых случаях.

Короче, инструмент простой, но охуенно полезный, когда нужно придушить свой же собственный разгоняющийся код, чтобы он не сломал всё вокруг. Пользуйся на здоровье, ебать!