Горутины и каналы

Материал из llmdocs — справочник разработчика
Портал: Go и Rust
Горутины и каналы
ЯзыкGo
МодельCSP (взаимодействующие последовательные процессы)
ПланировщикM:N, модель G-M-P
Начальный стек горутинынесколько КБ, растёт динамически
Ключевые конструкцииgo, chan, select, close
Сопутствующие пакетыsync, context, errgroup
Поиск гонокgo test -race

Горутины и каналы — встроенные в язык Go средства конкурентного программирования. Горутина — функция, выполняющаяся конкурентно с остальной программой под управлением планировщика среды исполнения Go; она намного легче потока операционной системы, и программа может держать сотни тысяч горутин одновременно. Канал — типизированная труба, через которую горутины передают друг другу значения и синхронизируются. Девиз подхода из официальной документации: «Не общайтесь через разделяемую память — разделяйте память через общение».[1]

Происхождение

Модель восходит к работе Тони Хоара «Взаимодействующие последовательные процессы» (CSP, 1978), где программы описываются как независимые процессы, обменивающиеся сообщениями по каналам.[3] Роб Пайк применял эти идеи в языках Newsqueak и Limbo до того, как они вошли в Go.

Горутины

Горутина запускается ключевым словом go перед вызовом функции:

go sendEmail(user)          // выполняется конкурентно, main продолжает работу
go func() {
    fmt.Println("анонимная горутина")
}()

Горутина стартует с маленьким стеком в несколько килобайт, который автоматически растёт и сжимается. Планировщик Go распределяет горутины по потокам ОС по схеме M:N (модель G-M-P): G — горутины, M — потоки ОС, P — логические процессоры, число которых по умолчанию равно числу ядер (GOMAXPROCS). Когда горутина блокируется на вводе-выводе или канале, поток переключается на другую. С Go 1.14 планировщик умеет принудительно прерывать долгие циклы, чтобы одна горутина не захватывала процессор.[6]

Каналы

ch := make(chan int)        // небуферизованный канал
buf := make(chan string, 10) // буферизованный на 10 элементов

ch <- 42      // отправить (блокируется, пока кто-то не примет)
v := <-ch     // принять
close(ch)     // закрыть: новых значений не будет
for v := range ch { ... }   // читать до закрытия
  • Небуферизованный канал синхронизирует отправителя и получателя: отправка завершается только в момент приёма.
  • Буферизованный канал позволяет отправить несколько значений без ожидания, пока буфер не заполнится.
  • Тип можно ограничить направлением: chan<- int — только отправка, <-chan int — только приём.
  • Отправка в закрытый канал вызывает панику; приём из закрытого возвращает нулевое значение и признак ok == false.

Оператор select

select ждёт сразу несколько операций с каналами и выполняет ту, что готова первой:[2]

select {
case msg := <-results:
    fmt.Println("результат:", msg)
case <-time.After(2 * time.Second):
    fmt.Println("таймаут")
case <-ctx.Done():
    return ctx.Err()   // отмена извне
}

Пример: пул обработчиков

func worker(id int, jobs <-chan int, results chan<- int, wg *sync.WaitGroup) {
    defer wg.Done()
    for j := range jobs {
        results <- j * j
    }
}

func main() {
    jobs, results := make(chan int, 100), make(chan int, 100)
    var wg sync.WaitGroup
    for w := 1; w <= 4; w++ {
        wg.Add(1)
        go worker(w, jobs, results, &wg)
    }
    for i := 1; i <= 9; i++ { jobs <- i }
    close(jobs)
    wg.Wait()
    close(results)
    for r := range results { fmt.Println(r) }
}

Четыре горутины разбирают задания из общего канала; sync.WaitGroup ждёт их завершения.

Сопутствующие средства

Средство Назначение
sync.WaitGroup дождаться группы горутин
sync.Mutex, RWMutex защитить общие данные, когда канал избыточен
sync.Once однократная инициализация
context.Context отмена и дедлайны, передаваемые по цепочке вызовов[4]
errgroup.Group (golang.org/x/sync) группа горутин с первой ошибкой и отменой
sync/atomic атомарные операции

Типичные ошибки

  • Утечка горутин — горутина вечно ждёт канал, в который никто не пишет. Лечится отменой через context и закрытием каналов.
  • Гонка данных — несколько горутин меняют переменную без синхронизации. Детектор гонок (go run -race, go test -race) находит такие места во время выполнения.[5] Подробнее — в статье Состояние гонки и блокировки.
  • Взаимная блокировка — все горутины ждут друг друга; среда исполнения сообщает «all goroutines are asleep — deadlock!».
  • Захват переменной цикла — до Go 1.22 переменная цикла была общей для всех итераций, и горутины, созданные в цикле, могли видеть одно и то же значение. С 1.22 каждая итерация получает свою переменную.

Сравнение с другими моделями

Язык Модель
Go горутины + каналы, вытесняющий планировщик в среде исполнения
Python asyncio: кооперативные сопрограммы в одном потоке
JavaScript цикл событий и промисы (см. Промисы и async/await (JS))
Rust async/await поверх выбираемой среды исполнения (см. async в Rust и Tokio)
Erlang/Elixir лёгкие процессы с почтовыми ящиками (модель акторов)

Главное отличие Go — горутины не требуют «раскрашивать» функции в синхронные и асинхронные: любая функция может быть запущена конкурентно.

См. также

Источники

  1. Effective Go — Concurrency
  2. The Go Programming Language Specification — Channel types, Select statements
  3. C. A. R. Hoare. Communicating Sequential Processes (1978)
  4. Go Concurrency Patterns: Context — The Go Blog
  5. Data Race Detector
  6. Go 1.14 Release Notes — asynchronous preemption