Что такое условная переменная (Condition Variable) в Go и для чего она используется?

Ответ

Условная переменная (sync.Cond) — это примитив синхронизации, который позволяет горутинам ждать (блокироваться) до тех пор, пока не будет выполнено некоторое условие. Она всегда используется в связке с мьютексом (sync.Mutex или sync.RWMutex).

Основная идея: одна или несколько горутин могут приостановить свое выполнение с помощью cond.Wait(), пока другая горутина не изменит условие и не оповестит ожидающих с помощью cond.Signal() или cond.Broadcast().

Ключевые методы:

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

Когда использовать?
Чаще всего sync.Cond применяется в сценариях типа "производитель-потребитель" (producer-consumer), где потребителям нужно ждать, пока в очереди появятся данные.

Пример:

package main

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

func main() {
    var mu sync.Mutex
    cond := sync.NewCond(&mu)
    queue := make([]int, 0)

    // Потребитель (consumer)
    go func() {
        for {
            mu.Lock()
            // Важно использовать цикл для проверки условия из-за "ложных пробуждений" (spurious wakeups)
            for len(queue) == 0 {
                fmt.Println("Потребитель: очередь пуста, жду...")
                cond.Wait() // Блокируется, пока производитель не добавит элемент
            }
            item := queue[0]
            queue = queue[1:]
            fmt.Printf("Потребитель: обработал %d, осталось в очереди: %dn", item, len(queue))
            mu.Unlock()
        }
    }()

    // Производитель (producer)
    for i := 0; i < 5; i++ {
        time.Sleep(1 * time.Second)
        mu.Lock()
        queue = append(queue, i)
        fmt.Printf("Производитель: добавил %d в очередьn", i)
        cond.Signal() // Оповещаем одного ждущего потребителя
        mu.Unlock()
    }

    time.Sleep(2 * time.Second)
}

Важные правила:

  1. Всегда блокируйте мьютекс перед вызовом Wait(), Signal() или Broadcast().
  2. Всегда проверяйте условие в цикле for, а не в if, чтобы корректно обработать ложные пробуждения и гарантировать, что условие действительно выполнено.