
Go (Golang) создан для эффективной параллельной и конкурентной работы. Его killer feature — легковесные потоки выполнения, называемые горутины (goroutines), и мощные средства синхронизации. Приглашаю разобраться подробно.
1. Что такое горутины и как они соотносятся с потоками?
Обычные потоки (threads):
В большинстве языков потоки создаются ОС, они "тяжёлые" (создание/переключение = дорого).Горутины (goroutines), это такой костыль go:
Это "зелёные" потоки Go — намного легче, чем системные потоки, планируются рантаймом Go (runtime).
На одном системном потоке могут работать тысячи горутин.
Создать горутину — просто:
go myFunc() // вызовет функцию в отдельной горутине
Важно:
Горутины могут выполняться параллельно, если Go-программа запущена на многоядерном CPU.
Количество системных потоков регулирует планировщик Go (через
GOMAXPROCS).
2. Проблема гонки данных (data race) и необходимость синхронизации
Если несколько горутин одновременно пишут/читают одну переменную — возникает гонка данных (data race). Это приводит к непредсказуемому поведению.
Пример гонки:
var counter int go func() { counter++ }() go func() { counter++ }()
Может случиться, что обе горутины увидят старое значение и запишут одинаковое новое.
3. Основные способы синхронизации данных в Go
A) Мьютексы (Mutex)
Мьютекс (mutual exclusion) — классическая примитивная блокировка.
В Go — тип sync.Mutex.
Применение:
import "sync" var mu sync.Mutex var counter int func inc() { mu.Lock() counter++ mu.Unlock() }
Только одна горутина в критической секции (между
Lock()иUnlock()).Важно: Всегда
UnlockпослеLock, иначе — deadlock!
В Go (как и в других языках), deadlock (взаимоблокировка) — это ситуация, при которой горутины навсегда застревают, ожидая друг друга или ресурсы, которые никогда не освободятся. В результате программа зависает и не может продолжить выполнение.
Что такое deadlock в Go
Deadlock возникает, когда:
Горутина ждет данные из канала, в который никто не пишет.
Несколько горутин ждут друг друга через каналы.
Мьютексы (или другие примитивы синхронизации) захвачены в таком порядке, что ресурсы никогда не освобождаются.
B) RWMutex
sync.RWMutex — позволяет нескольким читателям заходить одновременно, но писатель — только один и блокирует всех читателей.
var mu sync.RWMutex // Для чтения mu.RLock() // ... читать ... mu.RUnlock() // Для записи mu.Lock() // ... писать ... mu.Unlock()
C) Каналы (Channels)
Go-путь: синхронизация через обмен сообщениями, а не через блокировки.
ch := make(chan int) go func() { ch <- 42 // записать в канал (может заблокироваться) }() val := <-ch // получить из канала (может заблокироваться)
Канал может быть буферизированным или нет.
Позволяет строить очереди, worker pool, сигнализацию завершения.
D) sync/Atomic
Для простых операций над числами — атомарные операции (без мьютексов).
import "sync/atomic" var counter int64 atomic.AddInt64(&counter, 1) val := atomic.LoadInt64(&counter)
Быстрее, чем мьютексы, но только для примитивов (int, uint, pointer).
Не лучший вариант строить сложную логику через атомики
E) sync.WaitGroup
Используется для ожидания завершения группы горутин.
var wg sync.WaitGroup wg.Add(2) go func() { defer wg.Done() // ... }() go func() { defer wg.Done() // ... }() wg.Wait() // ждать завершения обеих горутин
F) sync.Once
Гарантирует, что функция будет вызвана ровно один раз (например, для инициализации singleton).
var once sync.Once once.Do(func() { // инициализация })
G) sync.Cond
Сложный, низкоуровневый механизм для организации очередей, сигнализации.
4. Часто используемые пакеты
sync— мьютексы, RWMutex, Once, WaitGroup, Cond, Poolsync/atomic— атомарные операции над числами и указателямиcontext— управление жизненным циклом (отмена/таймаут для горутин)runtime— низкоуровневое управление планировщиком (например,GOMAXPROCS)time— таймеры, Ticker для периодических событий
5. Пример: потокобезопасный counter
Рассмотрим три варианта:
1. С мьютексом
package main import ( "fmt" "sync" ) type SafeCounter struct { mu sync.Mutex value int } func (c *SafeCounter) Inc() { c.mu.Lock() c.value++ c.mu.Unlock() } func (c *SafeCounter) Value() int { c.mu.Lock() defer c.mu.Unlock() return c.value } func main() { counter := &SafeCounter{} var wg sync.WaitGroup for i := 0; i < 1000; i++ { wg.Add(1) go func() { counter.Inc() wg.Done() }() } wg.Wait() fmt.Println("Final value:", counter.Value()) }
2. С атомиками
package main import ( "fmt" "sync" "sync/atomic" ) type AtomicCounter struct { value int64 } func (c *AtomicCounter) Inc() { atomic.AddInt64(&c.value, 1) } func (c *AtomicCounter) Value() int64 { return atomic.LoadInt64(&c.value) } func main() { counter := &AtomicCounter{} var wg sync.WaitGroup for i := 0; i < 1000; i++ { wg.Add(1) go func() { counter.Inc() wg.Done() }() } wg.Wait() fmt.Println("Final value:", counter.Value()) }
3. Через канал (Go way)
package main import ( "fmt" "sync" ) type ChanCounter struct { ch chan int value int } func NewChanCounter() *ChanCounter { c := &ChanCounter{ ch: make(chan int), } go c.run() return c } func (c *ChanCounter) run() { for v := range c.ch { c.value += v } } func (c *ChanCounter) Inc() { c.ch <- 1 } func (c *ChanCounter) Close() { close(c.ch) } func (c *ChanCounter) Value() int { return c.value } func main() { counter := NewChanCounter() var wg sync.WaitGroup for i := 0; i < 1000; i++ { wg.Add(1) go func() { counter.Inc() wg.Done() }() } wg.Wait() counter.Close() fmt.Println("Final value:", counter.Value()) }
6. Советы и best practices
Мьютексы — используйте для защиты сложных структур, если нет необходимости в высокой скорости.
Атомики — для простых счётчиков, флагов и т.п.
RWMutex — если у вас много читателей и мало писателей.
Каналы — для построения concurrent pipeline, очередей и worker pool.
WaitGroup — всегда для ожидания завершения группы горутин.
Context — для управления отменой и таймаутами.
7. Частые ошибки
Не забыли
UnlockпослеLock? Используйтеdefer.Не делайте сложную бизнес-логику через атомики.
Не используйте глобальные переменные без защиты!
Не закрывайте канал, если кто-то еще пишет в него.
8. Заключение
Go — один из самых удобных языков для конкурентного программирования. Горутины дешевы, средства синхронизации богаты и просты в использовании.
Ключ к успеху — осознавать проблему гонки данных и правильно выбирать инструмент синхронизации под вашу задачу.
