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

Какие используешь средства синхронизации?

1.7 Middle🔥 242 комментариев
#Конкурентность и горутины#Производительность и оптимизация

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

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

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

Основные средства синхронизации в Go

В Go, несмотря на философию «Do not communicate by sharing memory; instead, share memory by communicating», существует богатый набор средств синхронизации, которые я применяю в зависимости от контекста задачи. Работая с конкурентным кодом, я выбираю инструменты, исходя из критериев: производительность, читаемость, безопасность и соответствие идиомам Go.

1. Каналы (channels) – основа коммуникации

Это первичный и предпочтительный механизм. Каналы не просто передают данные – они синхронизируют горутины. Я использую буферизованные и небуферизованные каналы, а также шаблоны типа Worker Pool, Fan-in/Fan-out.

// Небуферизованный канал для синхронного обмена
ch := make(chan int)
go func() { ch <- 42 }()
val := <-ch // Ожидание получения

// Буферизованный для асинхронности
bufCh := make(chan string, 10)

2. Мьютексы (mutexes) из пакета sync

Когда необходимо защитить общую память (структуру, кэш, счетчик), я применяю sync.Mutex и sync.RWMutex. Последний особенно эффективен для сценариев «много читателей, редкая запись».

type SafeCounter struct {
    mu    sync.RWMutex
    value int
}

func (c *SafeCounter) Increment() {
    c.mu.Lock()
    defer c.mu.Unlock()
    c.value++
}

func (c *SafeCounter) Get() int {
    c.mu.RLock()
    defer c.mu.RUnlock()
    return c.value
}

3. WaitGroup для ожидания завершения группы горутин

Идеален для параллельной обработки и сбора результатов.

var wg sync.WaitGroup
for i := 0; i < 5; i++ {
    wg.Add(1)
    go func(id int) {
        defer wg.Done()
        // Работа горутины
    }(i)
}
wg.Wait() // Ожидание всех

4. Once для однократного выполнения

sync.Once гарантирует, что операция (часто инициализация) выполнится ровно один раз, даже при конкурентных вызовах.

var initOnce sync.Once
func Initialize() {
    initOnce.Do(func() {
        // Инициализация один раз
    })
}

5. Cond (условные переменные)

sync.Cond использую реже, в специфичных случаях ожидания сигнала о изменении состояния несколькими горутинами. Например, реализация очереди с ограниченной емкостью.

6. Atomic операции

Пакет sync/atomic для низкоуровневых атомарных операций над примитивными типами. Применяю для высокопроизводительных счетчиков или флагов, где важна максимальная скорость.

var counter int32
atomic.AddInt32(&counter, 1) // Атомарное инкрементирование
val := atomic.LoadInt32(&counter)

7. Context для отмены и таймаутов

context.Context – это не только отмена, но и мощный механизм синхронизации временных и каскадных отмен в деревьях вызовов.

ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
select {
case <-ctx.Done():
    return ctx.Err()
case result := <-ch:
    // Обработка результата
}

Критерии выбора инструмента

  • Каналы – для передачи данных, организации конвейеров (pipelines), асинхронных событий.
  • Мьютексы – для защиты состояния в памяти (struct-полей, кэшей), когда операции быстрые и неблокирующие.
  • WaitGroup/Once – для управления жизненным циклом горутин и однократной инициализации.
  • Atomic – для микро-оптимизаций, когда производительность критчна.
  • Context – для управления временем жизни операций, особенно в сетевых запросах.

Я избегаю нативного использования примитивов ОС (semaphores, events), так как стандартная библиотека предоставляет более идиоматичные аналоги. Главный принцип: если задачу можно элегантно решить через коммуникацию (каналы, context) – выбираю этот путь. К прямым блокировкам (мьютексам) прибегаю, когда это явно эффективнее или логически оправдано. Комбинированный подход (например, защита мапы мьютексом с селектами по каналам) – обычная практика для построения надежных конкурентных систем.