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

Semaphore на каналах

2.0 Middle🔥 121 комментариев
#Основы Go

Условие

Реализуйте семафор на основе каналов Go. Семафор ограничивает количество одновременно выполняемых операций.

Интерфейс

type Semaphore struct {
    // ваши поля
}

func NewSemaphore(max int) *Semaphore
func (s *Semaphore) Acquire()
func (s *Semaphore) Release()

Требования

  • NewSemaphore создаёт семафор с указанным максимальным количеством
  • Acquire блокируется, если достигнут лимит
  • Release освобождает один слот

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

sem := NewSemaphore(3) // максимум 3 одновременные операции

for i := 0; i < 10; i++ {
    go func(id int) {
        sem.Acquire()
        defer sem.Release()
        // выполнение работы
        fmt.Printf("Worker %d working\n", id)
        time.Sleep(time.Second)
    }(i)
}

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

🐱
claude-haiku-4.5PrepBro AI23 мар. 2026 г.(ред.)

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

Решение: Semaphore на каналах

Основная идея

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

  • Канал размером max содержит токены
  • Acquire() — получить токен (блокируется если нет)
  • Release() — вернуть токен
  • Максимум max горутин могут работать одновременно

Простая реализация

type Semaphore struct {
    tokens chan struct{}
}

func NewSemaphore(max int) *Semaphore {
    return &Semaphore{
        tokens: make(chan struct{}, max),
    }
}

func (s *Semaphore) Acquire() {
    s.tokens <- struct{}{}
}

func (s *Semaphore) Release() {
    <-s.tokens
}

Как работает:

  1. Канал создан с буфером на max элементов
  2. Acquire() отправляет токен в канал
    • Если есть место в буфере — успешно (не блокируется)
    • Если буфер полон — блокируется до Release()
  3. Release() читает токен из канала, освобождая место

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

package main

import (
    "fmt"
    "time"
)

func main() {
    sem := NewSemaphore(3) // максимум 3 одновременные
    
    for i := 0; i < 10; i++ {
        go func(id int) {
            fmt.Printf("Worker %d waiting...\n", id)
            sem.Acquire()
            defer sem.Release()
            
            fmt.Printf("Worker %d working\n", id)
            time.Sleep(1 * time.Second)
            fmt.Printf("Worker %d done\n", id)
        }(i)
    }
    
    time.Sleep(10 * time.Second)
}

Вывод:

Worker 0 waiting...
Worker 1 waiting...
Worker 2 waiting...
Worker 3 waiting...
...
Worker 0 working
Worker 1 working
Worker 2 working
Worker 0 done
Worker 3 working
Worker 4 working
Worker 1 done
Worker 5 working
...

Видно, что максимум 3 работают одновременно.

Версия с context поддержкой (production-ready)

import "context"

type Semaphore struct {
    tokens chan struct{}
}

func NewSemaphore(max int) *Semaphore {
    return &Semaphore{
        tokens: make(chan struct{}, max),
    }
}

func (s *Semaphore) Acquire() {
    s.tokens <- struct{}{}
}

func (s *Semaphore) AcquireContext(ctx context.Context) error {
    select {
    case s.tokens <- struct{}{}:
        return nil
    case <-ctx.Done():
        return ctx.Err()
    }
}

func (s *Semaphore) Release() {
    <-s.tokens
}

Преимущества:

  • AcquireContext() позволяет отменить ожидание по timeout
  • Полезно для HTTP запросов с timeouts

Версия с счётчиком (альтернатива)

import "sync"

type Semaphore struct {
    mu    sync.Mutex
    count int
    max   int
    cond  *sync.Cond
}

func NewSemaphore(max int) *Semaphore {
    s := &Semaphore{
        max: max,
    }
    s.cond = sync.NewCond(&s.mu)
    return s
}

func (s *Semaphore) Acquire() {
    s.mu.Lock()
    defer s.mu.Unlock()
    
    for s.count >= s.max {
        s.cond.Wait()
    }
    s.count++
}

func (s *Semaphore) Release() {
    s.mu.Lock()
    defer s.mu.Unlock()
    
    s.count--
    s.cond.Signal()
}

Сравнение:

КритерийКаналMutex+Cond
Простота✓ Очень простоБолее сложно
Идиоматично для Go✓ ДаНет
Context поддержка✓ ЛегкоСложнее
Производительность✓ Немного вышеБлизко
Читаемость✓ ЛучшеХуже

Практический пример: Ограничение HTTP запросов

sem := NewSemaphore(5) // максимум 5 одновременных запросов

for _, url := range urls {
    go func(u string) {
        sem.Acquire()
        defer sem.Release()
        
        ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
        resp, err := http.NewRequestWithContext(ctx, "GET", u, nil)
        cancel()
        
        if err != nil {
            log.Printf("Error: %v", err)
            return
        }
        log.Printf("Success: %s", u)
    }(url)
}

Ключевые моменты

  • Буферизированный канал: ключ к реализации
  • struct{}: экономит память (0 байт)
  • Идиоматичность Go: каналы предпочтительнее мьютексов
  • Деферред Release: гарантирует освобождение при паник
  • Context поддержка: для отмены по timeout

Когда использовать

  • Ограничение параллельных HTTP запросов
  • Ограничение подключений к БД
  • Управление пулом рабочих горутин
  • Рейт-лимитинг

Это фундаментальный паттерн в Go для управления concurrency.