Что такое семафор и как его реализовать в Go?

Ответ

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

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

Реализация в Go

В Go семафоры удобно и идиоматично реализуются с помощью буферизованного канала. Емкость канала выступает в роли счетчика семафора.

  1. Захват (acquire): Отправка значения в канал. Если канал полон (все слоты заняты), горутина блокируется.
  2. Освобождение (release): Чтение значения из канала. Это освобождает место для другой ожидающей горутины.

Пример:

package main

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

func main() {
    // Создаем семафор, который разрешает не более 3 одновременных операций
    sem := make(chan struct{}, 3)
    var wg sync.WaitGroup

    // Запускаем 10 горутин, которые хотят выполнить работу
    for i := 0; i < 10; i++ {
        wg.Add(1)
        go func(id int) {
            defer wg.Done()

            sem <- struct{}{} // Захватываем слот семафора
            defer func() { <-sem }() // Освобождаем слот после завершения

            // Выполняем "тяжелую" работу
            fmt.Printf("Горутина %d начала работуn", id)
            time.Sleep(2 * time.Second)
            fmt.Printf("Горутина %d закончила работуn", id)
        }(i)
    }

    wg.Wait()
}

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

  • Ограничение параллелизма (Rate Limiting): Контроль количества одновременных запросов к внешнему API.
  • Управление пулом ресурсов: Ограничение числа активных соединений с базой данных.
  • Предотвращение перегрузки системы: Не давать ресурсоемким задачам выполняться всем сразу, чтобы не исчерпать CPU или память.