Что такое семафор в контексте Go? Как его можно реализовать и для чего он используется?

Ответ

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

Основные способы реализации в Go:

  1. Через буферизованный канал (классический способ): Канал используется как контейнер для "разрешений". Отправка в канал — захват разрешения, чтение из канала — освобождение.

    import (
        "fmt"
        "sync"
        "time"
    )
    
    func main() {
        var wg sync.WaitGroup
        // Семафор, ограничивающий параллельное выполнение до 3 горутин
        sem := make(chan struct{}, 3)
    
        for i := 0; i < 10; i++ {
            wg.Add(1)
            go func(i int) {
                sem <- struct{}{} // Захватываем "слот"
                defer func() {
                    <-sem // Освобождаем "слот"
                    wg.Done()
                }()
    
                // Выполняем "тяжелую" работу
                fmt.Printf("Работает горутина #%dn", i)
                time.Sleep(time.Second)
            }(i)
        }
    
        wg.Wait() // Ждем завершения всех горутин
    }
  2. С помощью пакета golang.org/x/sync/semaphore (рекомендуемый способ): Этот пакет предоставляет более явный и функциональный API.

    import (
        "context"
        "golang.org/x/sync/semaphore"
        "sync"
    )
    
    func main() {
        var wg sync.WaitGroup
        // Семафор с весом 3
        sem := semaphore.NewWeighted(3)
        ctx := context.Background()
    
        for i := 0; i < 10; i++ {
            wg.Add(1)
            go func(i int) {
                defer wg.Done()
                // Захватываем ресурс. Блокируется, если все ресурсы заняты.
                if err := sem.Acquire(ctx, 1); err != nil {
                    // Обработка ошибки (например, если контекст отменен)
                    return
                }
                defer sem.Release(1) // Освобождаем ресурс
    
                // ... работа ...
            }(i)
        }
        wg.Wait()
    }

Основные сценарии использования:

  • Ограничение количества одновременных HTTP-запросов к внешнему API.
  • Ограничение числа одновременных подключений к базе данных.
  • Контроль параллелизма при выполнении ресурсоемких задач (например, обработка файлов, вычисления).