← Назад к вопросам
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
}
Как работает:
- Канал создан с буфером на
maxэлементов Acquire()отправляет токен в канал- Если есть место в буфере — успешно (не блокируется)
- Если буфер полон — блокируется до
Release()
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.