За счет чего происходит синхронизация горутин?

Ответ

Синхронизация горутин в Go достигается с помощью двух основных подходов: каналов (channels) и примитивов синхронизации из пакета sync. Идиоматический Go-подход часто следует принципу "Не общайтесь, делясь памятью; делитесь памятью, общаясь" (Don't communicate by sharing memory; instead, share memory by communicating).

Основные механизмы:

  1. Каналы (Channels):

    • Каналы предоставляют безопасный способ обмена данными между горутинами. Они блокируют отправляющую горутину до тех пор, пока другая горутина не будет готова принять данные, и наоборот. Это обеспечивает автоматическую синхронизацию и предотвращает состояния гонки.
    • Используются для передачи данных, сигнализации о событиях, координации завершения работы и т.д.
    package main
    
    import "fmt"
    import "time"
    
    func worker(done chan bool) {
        fmt.Println("Рабочая горутина: Начало работы...")
        time.Sleep(1 * time.Second) // Имитация работы
        fmt.Println("Рабочая горутина: Работа завершена.")
        done <- true // Отправляем сигнал о завершении через канал
    }
    
    func main() {
        done := make(chan bool, 1) // Буферизованный канал для сигнала
        go worker(done)
    
        <-done // Блокируем main-горутину, пока не получим сигнал из канала
        fmt.Println("Main-горутина: Рабочая горутина завершила работу.")
    }
  2. WaitGroup (из пакета sync):

    • Используется для ожидания завершения группы горутин. Вы добавляете счетчик для каждой запущенной горутины (Add), каждая горутина уменьшает счетчик по завершении (Done), а вызывающая горутина ждет, пока счетчик не станет нулем (Wait).
    package main
    
    import (
        "fmt"
        "sync"
        "time"
    )
    
    func workerWG(id int, wg *sync.WaitGroup) {
        defer wg.Done() // Уменьшаем счетчик по завершении горутины
        fmt.Printf("Рабочая горутина %d: Начало работы...n", id)
        time.Sleep(500 * time.Millisecond)
        fmt.Printf("Рабочая горутина %d: Работа завершена.n", id)
    }
    
    func main() {
        var wg sync.WaitGroup
    
        for i := 1; i <= 3; i++ {
            wg.Add(1) // Увеличиваем счетчик для каждой новой горутины
            go workerWG(i, &wg)
        }
    
        wg.Wait() // Блокируем main-горутину, пока все горутины не завершатся
        fmt.Println("Main-горутина: Все рабочие горутины завершили работу.")
    }
  3. Mutex/RWMutex (из пакета sync):

    • Mutex (мьютекс): Используется для защиты общих данных от одновременного доступа нескольких горутин. Только одна горутина может владеть мьютексом в любой момент времени, обеспечивая эксклюзивный доступ к защищенному ресурсу.
    • RWMutex (мьютекс чтения/записи): Позволяет множеству горутин читать данные одновременно, но только одной горутине записывать данные (и блокирует все чтения во время записи).
    package main
    
    import (
        "fmt"
        "sync"
        "time"
    )
    
    var ( 
        counter int
        mu      sync.Mutex
    )
    
    func increment() {
        mu.Lock()   // Захватываем мьютекс
        defer mu.Unlock() // Освобождаем мьютекс при выходе из функции
        counter++
    }
    
    func main() {
        var wg sync.WaitGroup
    
        for i := 0; i < 1000; i++ {
            wg.Add(1)
            go func() {
                defer wg.Done()
                increment()
            }()
        }
    
        wg.Wait()
        fmt.Printf("Финальное значение счетчика: %dn", counter)
    }
  4. Atomic операции (из пакета sync/atomic):

    • Предоставляют низкоуровневые, атомарные операции для простых типов данных (например, int32, int64, uint32, uint64, pointer). Эти операции гарантированно выполняются целиком, без прерываний, что делает их безопасными для использования в конкурентной среде без необходимости мьютексов для простых счетчиков или флагов.
    package main
    
    import (
        "fmt"
        "sync"
        "sync/atomic"
    )
    
    var atomicCounter int32
    
    func main() {
        var wg sync.WaitGroup
    
        for i := 0; i < 1000; i++ {
            wg.Add(1)
            go func() {
                defer wg.Done()
                atomic.AddInt32(&atomicCounter, 1) // Атомарное инкрементирование
            }()
        }
    
        wg.Wait()
        fmt.Printf("Финальное значение атомарного счетчика: %dn", atomicCounter)
    }

Выбор между каналами и примитивами sync зависит от конкретной задачи. Каналы часто являются более идиоматичным и безопасным способом для координации и обмена данными, в то время как примитивы sync (мьютексы, WaitGroup) более подходят для защиты общих ресурсов или ожидания завершения группы задач.