Что такое `sync.Cond` и для чего он используется? Приведите пример и объясните ключевые моменты его работы.

Ответ

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

Назначение:
sync.Cond нужен, когда горутине нужно не просто дождаться разблокировки мьютекса, а дождаться, пока некое разделяемое состояние (защищенное этим мьютексом) не примет нужную форму.

Пример: Очередь с ожиданием
Представим, что у нас есть очередь, и несколько горутин-потребителей хотят забрать из нее элемент. Если очередь пуста, они должны уснуть и ждать, пока горутина-производитель не добавит что-то в очередь.

package main

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

var (
    mu    sync.Mutex
    cond  = sync.NewCond(&mu)
    queue []int
)

// Потребитель
func consumer(id int) {
    mu.Lock()
    // Важно использовать цикл для проверки условия!
    for len(queue) == 0 {
        fmt.Printf("Consumer %d is waiting...n", id)
        cond.Wait() // 1. Атомарно разблокирует мьютекс и усыпляет горутину
                   // 2. Когда горутину разбудят, она снова пытается заблокировать мьютекс
    }

    item := queue[0]
    queue = queue[1:]
    fmt.Printf("Consumer %d consumed %dn", id, item)
    mu.Unlock()
}

// Производитель
func producer() {
    time.Sleep(2 * time.Second) // Имитация работы
    mu.Lock()
    queue = append(queue, 1)
    fmt.Println("Producer added an item, signaling...")
    cond.Signal() // Будит ОДНУ случайную горутину, ожидающую на cond.Wait()
    // cond.Broadcast() // Разбудил бы ВСЕ ожидающие горутины
    mu.Unlock()
}

func main() {
    go consumer(1)
    go consumer(2)
    go producer()
    time.Sleep(3 * time.Second)
}

Ключевые правила использования:

  1. Wait() всегда в цикле: Горутина может быть разбужена ложно (spurious wakeup). Поэтому после пробуждения всегда нужно заново проверять условие (for !condition { cond.Wait() }).
  2. Блокировка мьютекса: Мьютекс должен быть заблокирован перед вызовом Wait(), Signal() или Broadcast().
  3. Signal() vs Broadcast():
    • Signal(): Будит одну (любую) из ожидающих горутин. Используется, когда любой из потребителей может выполнить работу (как в примере с очередью).
    • Broadcast(): Будит все ожидающие горутины. Используется, когда событие может быть важным для всех (например, изменение конфигурации, которое все должны перечитать).

Когда использовать sync.Cond, а когда — каналы?

  • sync.Cond: Идеален, когда несколько горутин ждут изменения состояния, защищенного мьютексом. Особенно полезен Broadcast для оповещения многих.
  • Каналы: Лучше подходят для прямой коммуникации между горутинами, передачи владения данными или для простой синхронизации (сигнал "готово").