Какие примитивы синхронизации предоставляет язык Go и пакет `sync`? Опишите их назначение и приведите примеры использования.

Ответ

Go предоставляет богатый набор инструментов для управления конкурентным доступом к данным. Основные примитивы находятся в пакете sync.

  1. Каналы (Channels)

    • Назначение: Основной идиоматический способ коммуникации и синхронизации между горутинами. Реализуют принцип "Не общайтесь, разделяя память; разделяйте память, общаясь" ("Don't communicate by sharing memory; share memory by communicating").
    • Пример:
      ch := make(chan string)
      go func() {
      ch <- "данные"
      }()
      msg := <-ch // Горутина блокируется, пока не получит данные из канала
      fmt.Println(msg)
  2. Мьютекс (sync.Mutex)

    • Назначение: Защита критической секции — участка кода, который в один момент времени может исполнять только одна горутина. Используется для предотвращения гонки данных при доступе к общим ресурсам.
    • Пример:
      
      var mu sync.Mutex
      var counter int

    // Внутри горутины mu.Lock() // Захватываем блокировку counter++ // Безопасно изменяем общие данные mu.Unlock() // Освобождаем блокировку

  3. Мьютекс чтения-записи (sync.RWMutex)

    • Назначение: Оптимизированный мьютекс для сценариев, где чтений гораздо больше, чем записей. Позволяет неограниченному числу горутин одновременно читать данные, но запись требует эксклюзивной блокировки.
    • Пример:
      
      var rwMu sync.RWMutex
      var config map[string]string

    // Чтение (может выполняться параллельно) rwMu.RLock() value := config["key"] rwMu.RUnlock()

    // Запись (эксклюзивный доступ) rwMu.Lock() config["key"] = "new_value" rwMu.Unlock()

  4. Группа ожидания (sync.WaitGroup)

    • Назначение: Блокировка горутины до тех пор, пока группа других горутин не завершит свою работу. Используется для ожидания завершения набора параллельных задач.
    • Пример:
      var wg sync.WaitGroup
      for i := 0; i < 5; i++ {
      wg.Add(1) // Увеличиваем счетчик горутин
      go func() {
          defer wg.Done() // Уменьшаем счетчик по завершении
          // ... какая-то работа
      }()
      }
      wg.Wait() // Ожидаем, пока счетчик не станет равен нулю
  5. Атомарные операции (sync/atomic)

    • Назначение: Низкоуровневые операции (сложение, загрузка, сохранение), которые выполняются без прерываний. Они намного быстрее мьютексов, но применимы только к простым числовым типам и указателям. Идеальны для счетчиков или флагов.
    • Пример:
      var counter int64
      atomic.AddInt64(&counter, 1) // Атомарный инкремент
  6. sync.Once

    • Назначение: Гарантирует, что определенный участок кода будет выполнен ровно один раз, независимо от количества параллельных вызовов. Часто используется для ленивой инициализации синглтонов или других ресурсов.
    • Пример:
      
      var once sync.Once
      var dbConnection *DB

    func GetConnection() *DB { once.Do(func() { dbConnection = connectToDatabase() }) return dbConnection }

  7. Условная переменная (sync.Cond)

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