Конкурентность в Go¶
TL;DR¶
go f()— запустить горутину (~2 KB stack, миллионы возможны).- Канал — типизированная очередь между горутинами.
- Unbuffered: send блокируется до receive.
sync.WaitGroup— ждать N горутин.sync.Mutex/RWMutex— критическая секция.context.Context— отмена и таймауты.go run -race— детектор гонок, всегда включай в CI.
Горутины¶
Стартуют через go, шедулятся runtime'ом Go (модель G/M/P). Стек растёт
динамически, начинается с ~2 KB.
Каналы¶
ch := make(chan int) // unbuffered
ch := make(chan int, 100) // buffered, capacity 100
ch <- 1 // send
v := <-ch // receive
v, ok := <-ch // ok=false если канал закрыт И буфер пуст
close(ch) // закрыть
for v := range ch { ... } // итерация до close
Правила: 1. Закрывает только sender. 2. Send в closed → panic. 3. Receive из closed → возвращает zero value моментально. 4. nil-канал в select никогда не срабатывает (полезно для динамического отключения case).
sync.WaitGroup¶
Ждём N горутин:
var wg sync.WaitGroup
for i := 0; i < 10; i++ {
wg.Add(1)
go func(i int) {
defer wg.Done()
// работа
}(i)
}
wg.Wait()
sync.Mutex / RWMutex¶
Защищаем shared state:
RWMutex для read-heavy: много RLock() параллельно, Lock() только
один.
atomic¶
Для одной переменной — atomic дешевле Mutex'а:
context.Context¶
Отмена и таймауты:
ctx, cancel := context.WithTimeout(parentCtx, 5*time.Second)
defer cancel()
select {
case result := <-doWork(ctx):
return result
case <-ctx.Done():
return nil, ctx.Err()
}
Конвенция: первый аргумент функции — ctx context.Context.
Worker pool¶
func worker(jobs <-chan Task, results chan<- Result) {
for j := range jobs {
results <- process(j)
}
}
jobs := make(chan Task, 100)
results := make(chan Result, 100)
for w := 0; w < N; w++ {
go worker(jobs, results)
}
for _, t := range tasks {
jobs <- t
}
close(jobs)
Параллельный fetch с лимитом¶
Fan-out/fan-in с семафором:
sem := make(chan struct{}, 10) // max 10 in-flight
var wg sync.WaitGroup
for _, url := range urls {
wg.Add(1)
sem <- struct{}{}
go func(u string) {
defer wg.Done()
defer func() { <-sem }()
fetch(u)
}(url)
}
wg.Wait()
Race condition и detector¶
Включай race detector в CI всегда (стоит ~2x памяти, ~10% CPU).
Deadlock¶
Простое правило: всегда захватывай несколько mutex'ов в одном порядке. Если нужен inverse — переписывай на каналы или actor-pattern.
📖 Связанные задачи: must-solve-100 → C-013..C-018, M-001..M-004.
🧪 Базовые задачи на многопоточку¶
Отдельная страница sprint-2/concurrency-tasks.md — 10 задач, которые встречаются на собесах в Авито, Озоне и подобных:
- Базовая deadlock-задача (channel + counter с гонкой) — 9 шагов фикса.
- «Найти максимальный чётный» — race condition + WaitGroup.
for i { go fmt.Println(i) }без WaitGroup.- Параллельные HTTP-запросы (google + avito).
- predictableFunc обёртка с context и тикером.
- Распараллеливание 10 000 запросов с WG и Mutex.
- Worker pool на 100k запросов и лимит 300 соединений.
- AsyncMerge каналов через дженерики.
- GetFiles параллельно с context отменой.
Do(ctx, []User)— fetchByName параллельно.
Каждая задача с условием, размышлением и развёрнутым решением.
Полезные материалы¶
📺 Видео
- Каналы в Go (RU) — buffered vs unbuffered, close, select.
- Горутины и каналы — практика (RU) — pipeline, fan-in/fan-out на примерах.
- Многопоточка в Go (плейлист) (RU) — большой курс, можно смотреть по темам.
- Go scheduler — как работает (RU) — модель G/M/P, work stealing.
- Go scheduler в деталях (RU) — preemption, syscalls, parking.
📚 Статьи
- Хабр — Планировщик Go (RU).
- Хабр — Планировщик Go (свежее) (RU).