> Что такое condition variable в синхронизации (Go)

Уровень: junior · Роль: backend · Язык: Go · Категория: Технические вопросы

Компании: VK

Стек: Go

> Пример ответа

Короткий ответ

Condition variable - это примитив синхронизации, который позволяет горутинам ждать наступления определённого условия и получать уведомление, когда оно выполняется. В Go condition variable реализована через sync.Cond и всегда используется вместе с мьютексом. Она решает проблему busy waiting: вместо активного опроса состояния горутина блокируется и просыпается только по сигналу.

Подробное объяснение

Condition variable состоит из трёх основных операций:

  • Wait() - атомарно разблокирует мьютекс и блокирует горутину до получения сигнала. После пробуждения горутина снова захватывает мьютекс.
  • Signal() - пробуждает одну горутину, ожидающую на condition variable.
  • Broadcast() - пробуждает все ожидающие горутины.

Ключевое свойство: Wait() выполняется атомарно относительно мьютекса. Это предотвращает гонку, когда горутина проверяет условие, а между проверкой и блокировкой условие меняется.

В Go sync.Cond создаётся через sync.NewCond(&mutex). Мьютекс должен быть передан при создании и не может быть заменён.

Типичный паттерн использования:

GO
mu.Lock()
for !condition {
cond.Wait()
}
// используем ресурс
mu.Unlock()

Цикл for обязателен, а не if, потому что:

  • Горутина может проснуться без сигнала (spurious wakeup).
  • Несколько горутин могут проснуться одновременно, и условие может снова стать ложным.

На практике

В Go condition variable используется реже, чем в других языках, потому что каналы покрывают большинство сценариев. Однако sync.Cond незаменим, когда:

  • Нужно уведомить несколько горутин о событии (Broadcast).
  • Нужно ожидать сложное условие, которое зависит от нескольких переменных.
  • Нужна более тонкая синхронизация, чем через каналы.

Примеры реального применения:

  • Очередь задач с ограниченной ёмкостью: продюсеры ждут места, потребители ждут задач.
  • Пул соединений: горутины ждут свободного соединения.
  • Кэш с инвалидацией: все читатели ждут обновления данных.

Важно помнить: Signal() и Broadcast() должны вызываться при захваченном мьютексе. Это не строгое требование, но гарантирует корректность, если условие зависит от состояния, защищённого мьютексом.

Пример кода

GO
package main
import (
"fmt"
"sync"
"time"
)
type Queue struct {
items []int
cond *sync.Cond
}
func NewQueue() *Queue {
return &Queue{
items: make([]int, 0),
cond: sync.NewCond(&sync.Mutex{}),
}
}
func (q *Queue) Push(item int) {
q.cond.L.Lock()
defer q.cond.L.Unlock()
q.items = append(q.items, item)
// Пробуждаем одного потребителя
q.cond.Signal()
}
func (q *Queue) Pop() int {
q.cond.L.Lock()
defer q.cond.L.Unlock()
// Обязательно цикл, а не if
for len(q.items) == 0 {
q.cond.Wait()
}
item := q.items[0]
q.items = q.items[1:]
return item
}
func main() {
q := NewQueue()
// Потребитель
go func() {
for i := 0; i < 3; i++ {
item := q.Pop()
fmt.Println("получено:", item)
}
}()
// Продюсер
for i := 1; i <= 3; i++ {
time.Sleep(100 * time.Millisecond)
q.Push(i)
}
time.Sleep(200 * time.Millisecond)
}

Как отвечать на собеседовании

Начни с определения: condition variable - примитив для ожидания условия с блокировкой вместо busy waiting. Затем объясни связку с мьютексом и почему Wait() атомарно разблокирует и блокирует. Обязательно упомяни цикл for вместо if - это ключевой момент, который проверяет понимание. Приведи пример из практики: очередь или пул ресурсов. Если спросят про альтернативы - скажи, что в Go каналы часто заменяют condition variable, но sync.Cond нужен для broadcast-уведомлений и сложных условий.

Что проверяет интервьюер

  • Понимание разницы между Signal() и Broadcast().
  • Знание, почему Wait() всегда вызывается в цикле.
  • Понимание атомарности Wait() относительно мьютекса.
  • Умение отличать condition variable от каналов и выбирать правильный инструмент.
  • Понимание опасности дедлоков при неправильном использовании.

Типичные ошибки

  • Использование if вместо for в цикле ожидания - приводит к гонкам и панике.
  • Вызов Signal() без захваченного мьютекса - может привести к потере сигнала.
  • Забывают, что Wait() после пробуждения снова захватывает мьютекс - код после Wait() выполняется под блокировкой.
  • Путают Signal() и Broadcast(): Signal() будит одну горутину, Broadcast() - все.
  • Пытаются заменить condition variable каналом там, где нужен broadcast - канал с закрытием не подходит для многократных уведомлений.

> ГОТОВЫ К СЛЕДУЮЩЕМУ СОБЕСЕДОВАНИЮ?

Запустите тренировочную сессию с ИИ и получите детальную обратную связь, чтобы увереннее проходить реальные интервью