Горутины и каналы
| Язык | 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 — горутины не требуют «раскрашивать» функции в синхронные и асинхронные: любая функция может быть запущена конкурентно.