Горутини¶
Горутина (goroutine) — це функція, що виконується паралельно з усім
іншим у програмі. Запускають її, поставивши go перед викликом. Це весь
синтаксис — функція виконується незалежно, а викликач продовжує роботу, не
чекаючи.
Горутини — це не потоки ОС. Середовище виконання Go мультиплексує багато горутин на невеликий пул потоків (планувальник M:N), тож вони надзвичайно дешеві — кілька кілобайтів стеку кожна, і їх можуть бути сотні тисяч. Створення горутини коштовно ближче до виклику функції, ніж до породження потоку.
З погляду Python: горутина схожа на задачу, заплановану в циклі подій, але без «забарвлення»
async/await— будь-яка функція може виконуватися в горутині — і середовище використовує справжні потоки ОС під капотом, тож горутини виконуються паралельно на кількох ядрах, а не лише конкурентно.
Головна горутина завершується — і забирає все з собою¶
Сам main виконується в горутині. Коли main повертає керування, програма
завершується негайно, не чекаючи на інші горутини. Тому це часто не
друкує нічого:
func main() {
go fmt.Println("hello from a goroutine")
// main повертає тут; програма може завершитися до запуску горутини
}
Горутина може не встигнути виконатися. Потрібна синхронізація — щоб
main дочекався завершення роботи. Сон — це не синхронізація (це здогад);
правильний інструмент — sync.WaitGroup.
Очікування через sync.WaitGroup¶
WaitGroup рахує незавершені горутини. Add(n) піднімає лічильник, кожна
горутина викликає Done() після завершення (через defer), а Wait()
блокує, доки лічильник не сягне нуля.
// checkHealth пінгує один сервіс. Sleep імітує реальну мережеву затримку.
func checkHealth(name string) string {
time.Sleep(100 * time.Millisecond)
return name + ": ok"
}
func main() {
services := []string{"api", "db", "cache"}
statuses := make([]string, len(services))
var wg sync.WaitGroup
for i, name := range services {
wg.Add(1)
go func() {
defer wg.Done()
statuses[i] = checkHealth(name) // кожна горутина пише у свій слот
}()
}
wg.Wait() // блокувати, доки всі три перевірки не завершаться
for _, s := range statuses {
fmt.Println(s)
}
}
// output:
// api: ok
// db: ok
// cache: ok
Ось навіщо потрібні горутини: перекривати незалежну повільну роботу.
Кожен checkHealth триває 100 мс. Один за одним — це ~300 мс; запущені як
горутини, вони всі чекають одночасно, тож увесь пакет завершується за
~100 мс. Замініть sleep на справжній HTTP-запит чи запит до БД — і це
справжній конкурентний перевіряч стану.
Дві речі тримають це коректним:
Wait()гарантує, що кожна перевірка завершилася, перш ніж ми читаємоstatuses.- Кожна горутина пише в різний слот зрізу, тож немає конкурентного
запису в ту саму пам'ять (немає гонитви — перевірте
go run -race).
Змінна циклу — на кожну ітерацію своя¶
Цикл вище захопив і i, і name у кожну горутину, і кожна ітерація має
власну копію — тож горутина для "db" справді бачить name == "db", а
не те значення, на якому цикл завершився. Це сучасна поведінка Go: кожна
ітерація циклу for має свіжу змінну циклу.
У старішому Go це було сумнозвісним багом — єдина спільна змінна на момент
запуску горутин зазвичай мала останнє значення циклу, тож кожна горутина
використала б "cache". Це обходили, передаючи значення як аргумент:
for i, name := range services {
wg.Add(1)
go func(i int, name string) { // старий обхід: передати копії як аргументи
defer wg.Done()
statuses[i] = checkHealth(name)
}(i, name)
}
Це все ще працює, і ви побачите його в старому коді, але потреби в ньому більше немає.
Горутини виконуються паралельно¶
За наявності кількох CPU горутини справді виконуються одночасно.
GOMAXPROCS керує тим, скільки їх може виконуватися водночас (за
замовчуванням — кількість CPU). Через справжню паралельність будь-який
спільний змінний стан потребує захисту — саме для цього є канали та пакет
sync, розглянуті далі.
Паніка горутини валить програму¶
Паніка в горутині, яку не відновлено всередині тієї самої горутини,
завалює весь процес — ви не можете відновити її з батьківської. Кожна
горутина відповідає за власний recover (див. panic та recover).
func main() {
var wg sync.WaitGroup
wg.Add(1)
go func() {
defer wg.Done()
panic("boom") // тут не відновлено
}()
wg.Wait()
}
// panic: boom
// (уся програма падає — відкладений wg.Done виконується під час розкручування,
// але ніщо в main не може це перехопити)
Виправлення — робити recover у самій горутині:
go func() {
defer wg.Done()
defer func() {
if r := recover(); r != nil {
fmt.Println("recovered:", r) // output: recovered: boom
}
}()
panic("boom")
}()
// main продовжує працювати
Швидка довідка¶
| Конструкція | Значення |
|---|---|
go f(args) |
виконати f паралельно; одразу повертає керування |
func main() повертає |
програма завершується, покидаючи інші горутини |
var wg sync.WaitGroup |
рахувати незавершені горутини |
wg.Add(n) / wg.Done() / wg.Wait() |
підняти / зменшити / блокувати-до-нуля |
| змінна циклу на ітерацію | кожна ітерація захоплює власну копію |
runtime.GOMAXPROCS |
макс. горутин, що виконуються паралельно |
runtime.NumGoroutine() |
поточна кількість горутин |