Что такое семафор в Go и в каких сценариях он применяется?

Ответ

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

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

Ключевой особенностью является semaphore.Weighted, который позволяет каждой операции "захватывать" (Acquire) и "освобождать" (Release) определенный "вес". Это делает его гибким инструментом для управления ресурсами.

Основные сценарии применения:

  • Ограничение параллелизма: Контроль количества одновременных HTTP-запросов к внешнему API, чтобы не превысить rate limit.
  • Управление пулом ресурсов: Ограничение числа активных подключений к базе данных.
  • Контроль ресурсоемких задач: Ограничение количества горутин, выполняющих тяжелые вычисления (например, обработка видео), чтобы избежать перегрузки CPU.
  • Реализация пула воркеров (Worker Pool): Семафор может определять максимальное количество одновременно работающих воркеров.

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

package main

import (
    "context"
    "fmt"
    "log"
    "runtime"
    "time"

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

func main() {
    // Создаем семафор с максимальным весом, равным количеству ядер CPU.
    // Это ограничит количество одновременно выполняемых "тяжелых" задач.
    maxWorkers := runtime.NumCPU()
    sem := semaphore.NewWeighted(int64(maxWorkers))
    ctx := context.Background()

    // Запускаем 20 задач, но одновременно будут выполняться не более `maxWorkers`.
    for i := 0; i < 20; i++ {
        // Захватываем "вес" 1. Если все ресурсы заняты, горутина блокируется здесь.
        if err := sem.Acquire(ctx, 1); err != nil {
            log.Printf("Failed to acquire semaphore: %v", err)
            break
        }

        go func(taskID int) {
            // Освобождаем ресурс в конце выполнения, чтобы другая горутина могла его занять.
            defer sem.Release(1)

            fmt.Printf("Задача #%d: выполняется работа...n", taskID)
            time.Sleep(2 * time.Second) // Имитация тяжелой работы
            fmt.Printf("Задача #%d: работа завершена.n", taskID)
        }(i)
    }

    // Ждем, пока все горутины освободят семафор.
    if err := sem.Acquire(ctx, int64(maxWorkers)); err != nil {
        log.Printf("Failed to acquire all semaphore weights: %v", err)
    }
    fmt.Println("Все задачи выполнены.")
}

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