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

Что такое Condition Variable?

2.0 Middle🔥 201 комментариев
#Конкурентность и горутины

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

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

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

Condition Variable (Переменная условия)

Condition Variable — это примитив синхронизации, который позволяет одной или нескольким горутинам ждать до тех пор, пока не произойдёт определённое событие, и других горутинам сигнализировать об этом событии.

Концепция

Condition Variable объединяет мьютекс и механизм ожидания/сигнала:

  • Горутина блокируется и освобождает мьютекс
  • Другая горутина может изменить состояние и разбудить ожидающую горутину
  • Когда горутина пробуждается, она снова захватывает мьютекс

Реализация в Go с sync.Cond

package main

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

func main() {
    var mu sync.Mutex
    cond := sync.NewCond(&mu)
    
    ready := false
    
    // Горутина 1: ждёт, пока ready станет true
    go func() {
        mu.Lock()
        for !ready { // Важно: используем for, а не if
            cond.Wait() // Ожидает и освобождает мьютекс
        }
        fmt.Println("Горутина 1: готово!")
        mu.Unlock()
    }()
    
    // Горутина 2: устанавливает ready и сигнализирует
    go func() {
        time.Sleep(1 * time.Second)
        mu.Lock()
        ready = true
        cond.Signal() // Разбудить одну ожидающую горутину
        mu.Unlock()
    }()
    
    time.Sleep(2 * time.Second)
}

Методы sync.Cond

1. Wait() — ожидание сигнала

cond.Wait()
// - Освобождает мьютекс
// - Блокируется до получения сигнала
// - При пробуждении заново захватывает мьютекс

2. Signal() — разбудить одну горутину

cond.Signal()
// Разбудит ровно одну горутину из ждущих
// Если нет ждущих, сигнал теряется (non-sticky)

3. Broadcast() — разбудить всех

cond.Broadcast()
// Разбудит все ждущие горутины

Практический пример: Пул рабочих

type Pool struct {
    mu       sync.Mutex
    items    []string
    notEmpty *sync.Cond
    notFull  *sync.Cond
    maxSize  int
}

func NewPool(maxSize int) *Pool {
    mu := &sync.Mutex{}
    p := &Pool{
        mu:       *mu,
        items:    []string{},
        notEmpty: sync.NewCond(mu),
        notFull:  sync.NewCond(mu),
        maxSize:  maxSize,
    }
    return p
}

func (p *Pool) Put(item string) {
    p.mu.Lock()
    
    // Ждём, пока очередь не станет свободной
    for len(p.items) >= p.maxSize {
        p.notFull.Wait()
    }
    
    p.items = append(p.items, item)
    p.notEmpty.Signal() // Пробудить потребителя
    p.mu.Unlock()
}

func (p *Pool) Get() string {
    p.mu.Lock()
    
    // Ждём, пока очередь не станет непуста
    for len(p.items) == 0 {
        p.notEmpty.Wait()
    }
    
    item := p.items[0]
    p.items = p.items[1:]
    p.notFull.Signal() // Пробудить производителя
    p.mu.Unlock()
    
    return item
}

Важные моменты

1. Всегда используй for, а не if

// ❌ Неправильно
mu.Lock()
if !condition {
    cond.Wait()
}
// Может быть spurious wakeup

// ✅ Правильно
mu.Lock()
for !condition {
    cond.Wait()
}
mu.Unlock()

2. Мьютекс должен быть захвачен перед Wait()

// ✅ Правильно
mu.Lock()
for !condition {
    cond.Wait()
}
mu.Unlock()

// ❌ Неправильно — паника
// cond.Wait() без захвата мьютекса

3. Spurious wakeup — горутина может проснуться без сигнала

Поэтому ВСЕГДА проверяй условие в цикле for.

Альтернатива в Go

В современном Go часто используют каналы вместо Condition Variable:

// Вместо Condition Variable используем канал
ready := make(chan bool)

go func() {
    <-ready // Ожидаем
    fmt.Println("Готово!")
}()

go func() {
    time.Sleep(1 * time.Second)
    ready <- true // Сигнализируем
}()

Каналы часто предпочтительнее, так как более гибкие и безопаснее, но Condition Variable полезны для сложных сценариев Producer-Consumer с буферизацией.

Что такое Condition Variable? | PrepBro