Синхронізація: sync та atomic¶
Канали — це бажаний у Go спосіб координувати горутини, але інколи вам просто
треба захистити частину спільного стану. Пакети sync та sync/atomic
дають класичні інструменти: м'ютекси, одноразову ініціалізацію та лічильники
без блокувань.
Проблема: стани гонитви¶
Коли дві горутини торкаються тієї самої змінної й принаймні одна пише, без синхронізації результат — це стан гонитви (data race), невизначена поведінка. Уявіть лічильник переглядів сторінки, який збільшують 1000 конкурентних запитів — це виглядає так, ніби він сягне 1000, але надійно цього не робить:
hits := 0
var wg sync.WaitGroup
for i := 0; i < 1000; i++ {
wg.Add(1)
go func() { defer wg.Done(); hits++ }() // ГОНИТВА: конкурентні записи
}
wg.Wait()
fmt.Println(hits) // непередбачувано: часто < 1000
hits++ — це читання-зміна-запис, три кроки, що можуть переплестися й
втратити оновлення, тож деякі запити «рахуються» один поверх одного.
sync.Mutex: взаємне виключення¶
Mutex пускає в захищену секцію лише одну горутину за раз. Lock перед
доступом до спільного стану, Unlock після (зазвичай через defer).
var mu sync.Mutex
hits := 0
var wg sync.WaitGroup
for i := 0; i < 1000; i++ {
wg.Add(1)
go func() {
defer wg.Done()
mu.Lock()
hits++ // один запит рахується за раз
mu.Unlock()
}()
}
wg.Wait()
fmt.Println(hits) // output: 1000
Тепер інкременти серіалізовано, тож кожен запит порахований — завжди 1000.
sync.RWMutex: багато читачів або один письменник¶
Коли читань значно більше за записи, RWMutex дозволяє будь-якій кількості
читачів працювати паралельно (RLock/RUnlock), тоді як записи
(Lock/Unlock) отримують винятковий доступ.
var mu sync.RWMutex
balance := 0
var wg sync.WaitGroup
// один письменник
wg.Add(1)
go func() {
defer wg.Done()
for i := 0; i < 100; i++ {
mu.Lock() // винятково
balance++
mu.Unlock()
}
}()
// три конкурентні читачі
for r := 0; r < 3; r++ {
wg.Add(1)
go func() {
defer wg.Done()
for i := 0; i < 100; i++ {
mu.RLock() // спільно — читачі не блокують одне одного
_ = balance
mu.RUnlock()
}
}()
}
wg.Wait()
fmt.Println(balance) // output: 100
Читачі виконуються паралельно один з одним; лише Lock письменника
змушує всіх інших чекати. Запустіть із go run -race — і це чисто.
sync.Once: виконати рівно раз¶
Once.Do(f) виконує f один раз, скільки б горутин його не викликали й
як би часто — стандартний спосіб ледачої потокобезпечної ініціалізації.
var once sync.Once
setup := func() { fmt.Println("init") }
for i := 0; i < 3; i++ {
once.Do(setup)
}
// output:
// init
sync/atomic: лічильники без блокувань¶
Для одного цілого атомарний тип простіший і швидший за м'ютекс. Типізовані
атоміки (atomic.Int64, atomic.Bool, …) несуть власну синхронізацію:
var hits atomic.Int64
var wg sync.WaitGroup
for i := 0; i < 1000; i++ {
wg.Add(1)
go func() { defer wg.Done(); hits.Add(1) }()
}
wg.Wait()
fmt.Println(hits.Load()) // output: 1000
Беріть атоміки для простих лічильників і прапорців; беріть м'ютекс, коли треба тримати кілька значень узгодженими разом.
Детектор гонитв¶
Go постачає детектор гонитв (race detector), який інструментує доступ
до пам'яті та повідомляє про гонитви під час виконання. Запускайте програму
чи тести з -race:
Запуск гонитвового циклу count++ з початку цієї статті з -race
повідомляє про неї:
$ go run -race .
==================
WARNING: DATA RACE
Read at 0x00c0000a0068 by goroutine 8:
main.main.func1()
Previous write at 0x00c0000a0068 by goroutine 9:
main.main.func1()
==================
Він ловить лише ті гонитви, що насправді трапилися під час запуску, тож
використовуйте його з тестами, що навантажують конкурентність. Це один із
найцінніших інструментів Go — заведіть звичку запускати тести з -race у
CI.
З погляду Python: немає GIL, що серіалізує байткод, тож код Go справді зазнає гонитв. Зворотний бік: справжня паралельність плюс першокласний детектор, щоб ловити помилки, які GIL приховав би.
Швидка довідка¶
| Інструмент | Застосування |
|---|---|
sync.Mutex (Lock/Unlock) |
винятковий доступ до спільного стану |
sync.RWMutex (RLock/RUnlock) |
багато читачів або один письменник |
sync.Once (Do) |
виконати ініціалізацію рівно раз |
sync.WaitGroup |
чекати завершення горутин |
atomic.Int64 тощо (Add/Load/Store) |
лічильники й прапорці без блокувань |
go run -race / go test -race |
виявляти стани гонитви |