← Назад к вопросам

Где применяется Semaphore?

1.0 Junior🔥 151 комментариев
#Конкурентность и горутины

Комментарии (1)

🐱
deepseek-v3.2PrepBro AI5 апр. 2026 г.(ред.)

Ответ сгенерирован нейросетью и может содержать ошибки

Применение семафоров в разработке на Go

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

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

1. Ограничение одновременного выполнения (Throttling)

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

package main

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

func main() {
    sem := make(chan struct{}, 3) // Семафор на 3 одновременно выполняемых задачи
    var wg sync.WaitGroup
    
    for i := 1; 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(1 * time.Second)
            fmt.Printf("Задача %d завершена\n", id)
        }(i)
    }
    
    wg.Wait()
    close(sem)
}

2. Реализация пула ресурсов

Семафоры удобны для управления пулом ограниченных ресурсов, таких как соединения с базой данных, сетевые сокеты или файловые дескрипторы. Каждый ресурс "выдаётся" горутине при захвате семафора.

type ConnectionPool struct {
    sem chan *Connection
}

func NewConnectionPool(size int) *ConnectionPool {
    pool := &ConnectionPool{
        sem: make(chan *Connection, size),
    }
    for i := 0; i < size; i++ {
        pool.sem <- &Connection{ID: i}
    }
    return pool
}

func (p *ConnectionPool) Acquire() *Connection {
    return <-p.sem // Захват соединения из пула
}

func (p *ConnectionPool) Release(conn *Connection) {
    p.sem <- conn // Возврат соединения в пул
}

3. Координация этапов в конвейерной обработке (Pipeline)

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

func processStage(input <-chan int, output chan<- int, limit int) {
    sem := make(chan struct{}, limit) // Семафор для ограничения параллелизма
    var wg sync.WaitGroup
    
    for data := range input {
        wg.Add(1)
        go func(d int) {
            defer wg.Done()
            sem <- struct{}{}
            defer func() { <-sem }()
            
            // Имитация тяжёлой обработки
            result := d * 2
            time.Sleep(100 * time.Millisecond)
            output <- result
        }(data)
    }
    
    wg.Wait()
    close(output)
}

4. Защита от состояния гонки (Race Conditions)

Хотя для защиты разделяемых данных обычно используют мьютексы (sync.Mutex), семафоры на основе каналов могут быть альтернативой в некоторых сценариях, особенно когда требуется более гибкое управление блокировками.

type ProtectedCounter struct {
    sem chan struct{}
    value int
}

func NewProtectedCounter() *ProtectedCounter {
    return &ProtectedCounter{
        sem: make(chan struct{}, 1), // Бинарный семафор (аналог мьютекса)
    }
}

func (c *ProtectedCounter) Increment() {
    c.sem <- struct{}{} // Захват
    c.value++
    <-c.sem // Освобождение
}

5. Реализация очереди с приоритетами

С помощью семафоров можно организовать доступ к ресурсам с учётом приоритетов, где высокоприоритетные задачи получают доступ быстрее.

type PrioritySemaphore struct {
    highPriority chan struct{}
    lowPriority  chan struct{}
}

func (ps *PrioritySemaphore) Acquire(highPriority bool) {
    if highPriority {
        select {
        case ps.highPriority <- struct{}{}:
            return
        default:
            <-ps.lowPriority
            ps.highPriority <- struct{}{}
        }
    } else {
        ps.lowPriority <- struct{}{}
    }
}

Ключевые преимущества семафоров в Go

  • Идиоматичность: реализация через каналы соответствует философии Go "не общайтесь разделением памяти, разделяйте память через общение".
  • Безопасность: каналы гарантируют безопасность для горутин и избегают распространённых ошибок при работе с примитивами низкого уровня.
  • Гибкость: можно создавать семафоры с разной ёмкостью, комбинировать с select для таймаутов и отмены.
  • Интеграция с контекстами: легко добавить отмену операций через context.Context.

Альтернативы и стандартные средства

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

  • Захват с различным "весом" (количеством единиц ресурса).
  • Интеграцию с context.Context для отмены и таймаутов.
  • Подробный контроль над лимитами.
import "golang.org/x/sync/semaphore"

func main() {
    sem := semaphore.NewWeighted(10) // Семафор на 10 единиц ресурса
    
    // Захват 3 единиц ресурса
    if err := sem.Acquire(context.Background(), 3); err != nil {
        log.Fatal(err)
    }
    defer sem.Release(3)
    
    // Критическая секция, использующая 3 единицы ресурса
}

Практические рекомендации

  • Для простых сценариев ограничения параллелизма достаточно канала с буфером.
  • Для сложных случаев с разными весами или приоритетами используйте golang.org/x/sync/semaphore.
  • Всегда освобождайте семафор через defer, чтобы избежать утечек при панике.
  • При работе с внешними API учитывайте не только лимиты параллелизма, но и RPS (запросы в секунду).

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