Горутины в Go: практические примеры с sync
Практическое руководство по работе с горутинами в Go с примерами sync.WaitGroup, sync.Mutex, context и sync.Pool. Около 60–90 минут выполнения для полного прогона примеров.
Статья была полезной?
Практическое руководство по работе с горутинами в Go с примерами sync.WaitGroup, sync.Mutex, context и sync.Pool. Около 60–90 минут выполнения для полного прогона примеров.
Статья была полезной?
Вы получите рабочие примеры использования горутин и синхронизации в Go, готовые к запуску на локальной машине. Примеры включают WaitGroup, Mutex, context-cancel и sync.Pool; полный прогон занимает примерно 60–90 минут в зависимости от скорости машины.
sync.WaitGroup для ожидания задач и предотвращения гонок данных.sync.Mutex и обнаружение race с -race.context.Context и шаблоны предотвращения утечек.sync.Pool для снижения аллокаций под нагрузкой.go в PATH, опционально pprof для профилирования.Горутина — легковесный поток управления в языке Go, запущенный ключевым словом go. В отличие от системных потоков, горутины имеют стартовый стек порядка ~2KB и растут/сжимаются по мере необходимости, что позволяет запускать десятки и сотни тысяч горутин в одном процессе при достаточном объёме памяти. Горутины выполняются планировщиком runtime Go, который мультиплексирует их на системные OS-потоки.
Горутины эффективны для I/O-bound задач (сетевые запросы, файловые операции), параллельной обработки задач и распределения работы между воркерами. Для CPU-bound задач полезно комбинировать горутины с GOMAXPROCS. Практические числа: 100–10000 горутин безопасно на машине с 4 ядрами и 8 ГБ ОЗУ при небольшом контекстном состоянии каждой; миллионы горутин требуют значительной памяти и тщательного профилирования. Для обмена данными используйте каналы или sync-примитивы; избегайте глобального состояния без синхронизации.
Утечка горутины — состояние, когда горутина блокируется навсегда и не завершается. Основные причины: ожидание на канал без отправителя, забытый канал отмены, блокировка на внешнем ресурсе. Профилактика: явная отмена через context.Context, использование select с ctx.Done(), таймауты и аккуратное применение WaitGroup. Также применяйте инструменты: go test -race и pprof для поиска висящих стеков и утечек.
Команда: сохраните файл main.go и выполните go run main.go. Пример демонстрирует, как main может завершить программу раньше, чем завершаются горутины.
// main.go
package main
import (
"fmt"
"time"
)
func main() {
go func() {
time.Sleep(100 * time.Millisecond)
fmt.Println("hello from goroutine")
}()
// main завершается сразу
}Ожидаемый вывод (если неправильно синхронизирован):
// Ничего не выведется, программа завершится до печатиТиповая ошибка: горутина не успевает выполнить работу — вы не увидите вывод. Причина: main завершил процесс.
Исправление: использовать sync.WaitGroup или time.Sleep для теста. Пример с WaitGroup:
// main.go (fix)
package main
import (
"fmt"
"sync"
"time"
)
func main() {
var wg sync.WaitGroup
wg.Add(1)
go func() {
defer wg.Done()
time.Sleep(100 * time.Millisecond)
fmt.Println("hello from goroutine")
}()
wg.Wait()
}Ожидаемый вывод после исправления:
hello from goroutineКоманда: go run counter.go. Пример считает сумму с параллельными инкрементами и показывает гонку данных и её устранение.
// counter.go (vulnerable)
package main
import (
"fmt"
"sync"
)
func main() {
var wg sync.WaitGroup
cnt := 0
for i := 0; i < 1000; i++ {
wg.Add(1)
go func() {
defer wg.Done()
cnt++
}()
}
wg.Wait()
fmt.Println("count:", cnt)
}Ожидаемый нежелательный вывод (неопределённо):
count: 742 // значение может быть меньше 1000 из-за гонкиТиповая ошибка: гонка данных — несколько горутин одновременно модифицируют cnt. Запуск с детектором гонок:
go run -race counter.goДетектор выведет сообщение о data race. Исправление: использовать sync.Mutex или атомарные операции.
// counter.go (fixed)
package main
import (
"fmt"
"sync"
)
func main() {
var wg sync.WaitGroup
var mu sync.Mutex
cnt := 0
for i := 0; i < 1000; i++ {
wg.Add(1)
go func() {
defer wg.Done()
mu.Lock()
cnt++
mu.Unlock()
}()
}
wg.Wait()
fmt.Println("count:", cnt)
}Ожидаемый корректный вывод:
count: 1000Типовая ошибка после фикса: блокировки, которые приводят к дедлоку, если забыть defer mu.Unlock(). Фикс: всегда использовать defer сразу после Lock().
Команда: go run cancel.go. Пример с сервероподобной работой, где горутина слушает задач и должна корректно завершаться при отмене.
// cancel.go
package main
import (
"context"
"fmt"
"time"
)
func worker(ctx context.Context, id int) {
for {
select {
case <-ctx.Done():
fmt.Println("worker", id, "stopping")
return
default:
// имитируем работу
time.Sleep(100 * time.Millisecond)
}
}
}
func main() {
ctx, cancel := context.WithCancel(context.Background())
for i := 0; i < 3; i++ {
go worker(ctx, i)
}
time.Sleep(350 * time.Millisecond)
cancel()
// даём время на завершение
time.Sleep(100 * time.Millisecond)
}Ожидаемый вывод:
worker 0 stopping
worker 1 stopping
worker 2 stoppingТиповая ошибка: забыли слушать ctx.Done(). Тогда горутины останутся висящими и будут потреблять ресурсы. Фикс: в каждой горутине всегда добавляйте ветвь для отмены через select и применяйте таймауты, если операция блокирующая.
Команда: go run pool.go. Пример показывает переиспользование буферов для снижения давления на сборщик мусора при интенсивном создании объектов.
// pool.go
package main
import (
"bytes"
"fmt"
"sync"
)
var bufPool = sync.Pool{
New: func() interface{} { return new(bytes.Buffer) },
}
func worker(id int, n int, wg *sync.WaitGroup) {
defer wg.Done()
for i := 0; i < n; i++ {
b := bufPool.Get().(*bytes.Buffer)
b.Reset()
b.WriteString(fmt.Sprintf("worker %d: %d", id, i))
// используем буфер
_ = b.String()
bufPool.Put(b)
}
}
func main() {
var wg sync.WaitGroup
workers := 50
items := 2000
wg.Add(workers)
for i := 0; i < workers; i++ {
go worker(i, items, &wg)
}
wg.Wait()
fmt.Println("done")
}Ожидаемый вывод:
doneТиповая ошибка: неверно обрабатывать значение из Pool (например, привести к неверному типу) или положить в Pool объект в неконсистентном состоянии. Фикс: всегда вызывать Reset() и проверять приведение типов. Если New возвращает nil, и вы ожидаете не-nil, инициализируйте объект в New.
Команды для диагностики:
go run -race program.go — запуск с детектором гонок.go test -run TestX -bench . -benchmem — бенчмарки с памятью.go test -cpuprofile cpu.prof и go tool pprof cpu.prof.Пример команды и возможный вывод race detector:
go run -race counter.go
==================
WARNING: DATA RACE
Read at 0x00c0000a2008 by goroutine 8:
main.main.func1()
/home/user/counter.go:15 +0x3a
Previous write at 0x00c0000a2008 by goroutine 7:
main.main.func1()
/home/user/counter.go:15 +0x5a
==================
Found 1 data race(s)
exit status 66Типовая ошибка: отсутствие pprof или невозможность собрать профиль в production. Фикс: откройте доступ к профилю через защищённый endpoint, используйте сбор профилей в контролируемой среде или снимайте дампы стеков через runtime/pprof по расписанию.

Скриншот стека висящих горутин в pprof

Скрин вывода go run -race с сообщением data race
sync.WaitGroup для координации завершения.sync.Mutex или атомиками.context.sync.Pool под нагрузкой.Для практических руководств по деплою и системному запуску посмотрите материалы на Golang и по CI/CD на DevOps. Также полезны статьи о профилировании и оптимизации памяти.
Утечка горутины обычно проявляется как постоянный рост числа активных goroutine по времени или зависание сервиса. Для обнаружения снимайте профиль goroutine с помощью pprof (endpoint или runtime/pprof) и смотрите стек каждой висящей горутины. Если в стеке есть операции чтения с канала без отправителя или ожидание на внешнем ресурсе, это указывает на утечку. Также используйте go test -race, чтобы исключить гонки, которые могут привести к незавершению работы.
Выбор зависит от задачи. Каналы хороши для передачи данных и построения конвейеров; они делают код декларативным и помогают избежать явных блокировок. sync.Mutex эффективен для простой защиты небольшого участка кода или структуры данных; он быстрее, когда требуется низкий оверхед и простая критическая секция. Под высокими нагрузками замеряйте оба подхода и используйте инструменты профилирования для принятия решения.
sync.Pool помогает с производительностью?sync.Pool снижает число аллокаций и работу GC, переиспользуя объекты между горутинами. Это особенно заметно при короткоживущих объектах в hot-path. Однако Pool не гарантирует сохранение объектов между garbage collection'ами, поэтому не следует хранить состояние, которое важно для корректности; используйте Pool только для временных буферов и объектов, которые можно безопасно реинициализировать.
-race?Запускайте -race на этапах тестирования и CI для обнаружения data race на ранней стадии. Детектор полезен при параллельных изменениях общих структур и при добавлении новых горутин. На production-сборках -race обычно не используют из-за ухудшения производительности, но его результаты обязаны лечиться до релиза.
Количество зависит от доступной памяти и потребления стека каждой горутиной. При стартовом стеке ~2KB теоретически можно создать сотни тысяч горутин на машине с достаточным объёмом ОЗУ. На практике важно профилировать: допустимо 100–10000 горутин для обычных сервисов; миллионы требуют специальной архитектуры и экономии памяти. Всегда измеряйте использование памяти и задержки при масштабировании.
Комментарии (0)
Войдите или зарегистрируйтесь, чтобы оставить комментарий
Загрузка комментариев…