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, Pool -
sync/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 — один из самых удобных языков для конкурентного программирования. Горутины дешевы, средства синхронизации богаты и просты в использовании.
Ключ к успеху — осознавать проблему гонки данных и правильно выбирать инструмент синхронизации под вашу задачу.
ссылка на оригинал статьи https://habr.com/ru/articles/933464/
Добавить комментарий