5. Sync primitives и их стоимость
Сравнительная стоимость примитивов синхронизации в Go
В лекции 4. Контекстные переключения мы детально исследовали физику смены задач и выяснили, что каждое переключение горутины или потока ОС обходится машине в измеримую цену — от десятков наносекунд в пространстве пользователя до единиц и десятков микросекунд при погружении в ядро. Теперь мы переходим к исследованию первопричины большинства контекстных переключений в высоконагруженных системах — к примитивам синхронизации. Именно они заставляют горутины притормаживать, уступать процессор, парковаться в системных очередях и бороться за доступ к разделяемым ресурсам.
В распоряжении разработчика на Go находится богатый и выразительный арсенал средств координации: классический взаимный замок sync.Mutex, многопоточный sync.RWMutex, аппаратные инструкции sync/atomic, идиоматичные каналы (chan), барьеры ожидания sync.WaitGroup, однократная инициализация sync.Once и условные переменные sync.Cond. В повседневной разработке выбор между ними часто диктуется поверхностной интуицией: «каналы идиоматичнее мьютексов», «RWMutex всегда быстрее обычного Mutex», «atomic вообще бесплатен».
Однако в реальных распределенных сервисах под предельными нагрузками эти стереотипы разбиваются о суровую физику кремния. Неверно выбранный примитив синхронизации может обернуться скрытым падением пропускной способности на 30–50% и неконтролируемым раздуванием хвостовой задержки p99. Senior-инженер обязан оперировать точными количественными характеристиками каждого инструмента: знать его стоимость на быстром и медленном путях, понимать внутреннее устройство в рантайме Go и ясно представлять, что происходит на уровне кэш-линий процессора и шины межъядерной когерентности MESI (5. Mechanical sympathy в backend разработке).
Эта лекция дает всесторонний сравнительный анализ всех ключевых примитивов синхронизации, связывая их с моделью планировщика G-M-P (1. Scheduler Go. G-M-P модель, 1. Scheduler Go. G-M-P модель) и системными вызовами ядра Linux (1. Системные вызовы и их стоимость). Материал послужит фундаментом для батла «мьютексы против каналов» (6. Mutex vs channel) и методологии поиска блокировок (7. Contention и lock profiling).
Mutex: быстрый путь, медленный путь и голодание
Мьютекс sync.Mutex — самый распространенный и фундаментальный примитив синхронизации в Go. Его исходное определение в файле src/sync/mutex.go выглядит обманчиво минималистичным:
type Mutex struct {
state int32 // Битовая маска состояния: заблокирован, голодание, число ожидающих
sema uint32 // Семафор рантайма для сна и пробуждения горутин через futex
}
Несмотря на размер всего в 8 байт, алгоритм работы sync.Mutex представляет собой сложнейший адаптивный механизм, разделенный на быстрый путь (Fast Path) и медленный путь (Slow Path).
flowchart TD
classDef process fill:#1e293b,stroke:#38bdf8,stroke-width:1px,color:#f8fafc
classDef success fill:#064e3b,stroke:#34d399,stroke-width:1px,color:#f8fafc
classDef error fill:#7f1d1d,stroke:#f87171,stroke-width:1px,color:#f8fafc
classDef warning fill:#78350f,stroke:#fbbf24,stroke-width:1px,color:#f8fafc
classDef entry fill:#1e3a5f,stroke:#38bdf8,stroke-width:2px,color:#f8fafc
classDef aux fill:#334155,stroke:#64748b,stroke-width:1px,color:#f8fafc
classDef system fill:#312e81,stroke:#a78bfa,stroke-width:1px,color:#f8fafc
classDef data fill:#132d29,stroke:#2dd4bf,stroke-width:1px,color:#f8fafc
classDef network fill:#881337,stroke:#fb7185,stroke-width:1px,color:#f8fafc
classDef accent fill:#1e1b4b,stroke:#818cf8,stroke-width:1px,color:#f8fafc
Start["Вызов Mutex.Lock()"]:::warning --> FastPath{"CAS(&state, 0, mutexLocked)?<br/>(Мьютекс полностью свободен?)"}
FastPath -- "Да (Fast Path)" --> Acquired["Замок захвачен!<br/>Время: ~10-20 нс (1 CAS)"]
FastPath -- "Нет (Slow Path)" --> CheckSpin{"Можно крутиться в Spin?<br/>(многоядерная система, GOMAXPROCS>1,<br/>локальная runq пуста, мало итераций)"}
CheckSpin -- Да --> DoSpin["PAUSE инструкция CPU (Active Spinning)"]
DoSpin --> FastPath
CheckSpin -- Нет --> SleepPhase["runtime.semacquire(&sema):<br/>1. Вызов gopark (статус _Gwaiting)<br/>2. Системный вызов futex(FUTEX_WAIT)<br/>3. Поток M засыпает в ядре"]
SleepPhase --> WokenUp["Пробуждение через futex(FUTEX_WAKE) / semrelease"]
WokenUp --> StarveCheck{"Ждали мьютекс > 1 мс?"}
StarveCheck -- Да --> EnterStarve["Переход в Starvation Mode (режим голодания)"]
StarveCheck -- Нет --> Acquired
EnterStarve --> HandOff["Прямая передача замка голове очереди (Handoff)"]
HandOff --> Acquired
1. Быстрый путь (Fast Path)
Если мьютекс свободен и за него нет конкуренции, вызов sync.Mutex.Lock() сводится к единственной атомарной инструкции процессора: Compare-And-Swap (CAS). Она атомарно проверяет, равен ли state нулю, и выставляет бит mutexLocked:
// Внутренняя реализация Fast Path
if atomic.CompareAndSwapInt32(&m.state, 0, mutexLocked) {
return
}
На быстром пути нет ни обращений к планировщику, ни системных вызовов ядра. При отсутствии contention (например, когда GOMAXPROCS=1 или при низкой конкурентности) мьютекс практически всегда идет по быстрому пути, и стоимость операции составляет всего 10–20 наносекунд (время выполнения инструкции LOCK CMPXCHG на x86).
2. Медленный путь (Slow Path)
Если мьютекс уже удерживается другой горутиной, выполнение немедленно переходит в функцию lockSlow. Здесь рантайм Go применяет двухфазную стратегию:
- Фаза активного спин-ожидания (Active Spinning):
Засыпание горутины в ядре ОС стоит дорого (~1–5 мкс). Поэтому, если блокировка была кратковременной, выгоднее несколько тактов «покрутиться» на процессоре, непрерывно опрашивая статус мьютекса в надежде на скорое освобождение.
Однако Go разрешает спиннинг только при строгом соблюдении условий:- В системе доступно более одного процессора (
GOMAXPROCS > 1); - Поток
Mудерживает процессорP, и его локальная очередьrunqпуста; - Число итераций спиннинга жестко ограничено (не более 4 итераций с процессорной инструкцией
PAUSEна x86).
Если мьютекс освобождается во время спиннинга, горутина мгновенно захватывает его, сэкономив тысячи тактов на переключении контекста.
- В системе доступно более одного процессора (
- Фаза пассивного ожидания (Futex Sleep):
Если спиннинг не принес результата, горутина инкрементирует счетчик ожидающих в полеstateи вызывает процедуру рантаймаruntime.semacquire. Рантайм переводит горутину в статус_Gwaitingчерезgopark, а системный поток ядраMуходит в глубокий сон через системный вызовfutex(FUTEX_WAIT)в Linux. - Фаза освобождения (
Unlock):
При вызовеMutex.Unlockвладелец сбрасывает битmutexLocked. Если счетчик ожидающих не равен нулю, вызывается процедураruntime.semrelease, которая с помощью системного вызоваfutex(FUTEX_WAKE)пробуждает спящую горутину.
Режим голодания (starvation mode)
В стандартном («нормальном») режиме освобожденный мьютекс не передается напрямую спящей горутине: проснувшаяся горутина вступает в честную конкуренцию с новыми горутинами, которые только что подошли к Lock() на быстром пути. Поскольку новые горутины уже выполняются на ядрах CPU и их данные прогреты в кэше, они почти всегда выигрывают гонку у только что проснувшейся горутины.
Чтобы предотвратить бесконечное откладывание спящих задач, начиная с Go 1.9 был внедрен режим голодания (starvation mode):
- Если проснувшаяся горутина обнаруживает, что она провела в очереди ожидания дольше 1 миллисекунды, она принудительно переводит мьютекс в режим голодания (выставляется бит
mutexStarving). - В режиме голодания входящие новые горутины лишаются права захватывать мьютекс на быстром пути и крутиться в спиннинге: они принудительно отправляются в хвост очереди ожидания.
- При вызове
Unlockвладелец замка выполняет прямую эстафетную передачу владения (Handoff) первой горутине в очереди семафора. - Мьютекс возвращается в нормальный режим, когда очередь опустеет или время ожидания последней горутины окажется менее 1 мс.
Состояния Mutex
flowchart LR
classDef process fill:#1e293b,stroke:#38bdf8,stroke-width:1px,color:#f8fafc
classDef success fill:#064e3b,stroke:#34d399,stroke-width:1px,color:#f8fafc
classDef error fill:#7f1d1d,stroke:#f87171,stroke-width:1px,color:#f8fafc
classDef warning fill:#78350f,stroke:#fbbf24,stroke-width:1px,color:#f8fafc
classDef entry fill:#1e3a5f,stroke:#38bdf8,stroke-width:2px,color:#f8fafc
classDef aux fill:#334155,stroke:#64748b,stroke-width:1px,color:#f8fafc
classDef system fill:#312e81,stroke:#a78bfa,stroke-width:1px,color:#f8fafc
classDef data fill:#132d29,stroke:#2dd4bf,stroke-width:1px,color:#f8fafc
classDef network fill:#881337,stroke:#fb7185,stroke-width:1px,color:#f8fafc
classDef accent fill:#1e1b4b,stroke:#818cf8,stroke-width:1px,color:#f8fafc
Unlocked["Разблокирован (state = 0)"]:::process -->|"CAS Lock"| Locked["Заблокирован<br/>Владелец исполняется"]
Locked -->|"Unlock"| Pending{"Есть ожидающие?"}
Pending -- "Нет" --> Unlocked
Pending -- "Да" --> Handoff["Передача ожидающему<br/>или пробуждение"]
Handoff --> Locked
Locked -->|"Ожидание > 1 мс"| Starvation["Режим голодания<br/>(mutexStarving = 1)"]
Starvation -->|"Handoff без спиннинга"| Locked
RWMutex: читатели и писатели
Тип sync.RWMutex реализует классический паттерн Single-Writer / Multiple-Readers. Он позволяет неограниченному количеству читателей одновременно удерживать блокировку на чтение, предоставляя писателю право эксклюзивного доступа.
Внутренняя анатомия sync.RWMutex в src/sync/rwmutex.go:
type RWMutex struct {
w Mutex // Внутренний Mutex для сериализации писателей
writerSem uint32 // Семафор ожидания писателя (ждет завершения читателей)
readerSem uint32 // Семафор ожидания читателей (ждут освобождения писателем)
readerCount atomic.Int32 // Счетчик активных читателей (+ маркер ожидания писателя)
readerWait atomic.Int32 // Количество читателей, которые должны выйти перед писателем
}
Механика работы
- Захват читателем (
RLock/RUnlock):
Быстрый путьRLockтривиален: атомарный инкремент счетчикаreaderCount.
Если счетчик положителен (нет активных и ожидающих писателей), читатель мгновенно входит в критическую секцию. Стоимость составляет всего 10–20 наносекунд.
При выходеRUnlockатомарно декрементируетreaderCount. - Захват писателем (
Lock/Unlock):
Писатель сначала захватывает внутренний мьютексw, блокируя конкурирующих писателей.
Затем писатель атомарно вычитает изreaderCountгигантскую константуrwmutexMaxReaders($1 \ll 30 = 1\,073\,741\,824$). С этого момента счетчик становится глубоко отрицательным. Новые читатели, видя отрицательныйreaderCount, немедленно блокируются на семафореreaderSem.
Если в момент прихода писателя уже были активные читатели (readerCount > 0), их количество копируется вreaderWait, а писатель засыпает на семафореwriterSem. Последний выходящий читатель вRUnlockобнуляетreaderWaitи пробуждает писателя.
Atomic: самый дешёвый примитив
Пакет sync/atomic предоставляет прямой доступ к аппаратным атомарным инструкциям кремниевого процессора:
- x86-64: инструкции с префиксом шинной блокировки
LOCK ADD,LOCK CMPXCHG,XCHG; - ARM64: эксклюзивные пары инструкций
LDXR/STXRили атомарные расширения Large System Extensions (LSE:LDADD,SWP,CAS).
Операции sync/atomic выполняются целиком на уровне контроллера кэш-памяти процессора (Cache Coherence Engine) без захвата программных мьютексов, без обращения к планировщику Go и без парковки горутин:
atomic.Load(чтение, напримерLoadInt64): на современных процессорах x86 выровненное 64-битное чтение аппаратно атомарно по умолчанию, стоимость составляет 1–2 наносекунды.atomic.Store(запись, напримерStoreInt64): атомарная запись со сбросом store-буфера, стоимость 2–5 наносекунд.atomic.Add(инкремент, напримерAddInt64): операция Read-Modify-Write с аппаратной блокировкой линии кэша, стоимость 5–15 наносекунд.CAS(Compare-And-Swap, напримерCompareAndSwapInt64): атомарная проверка и замена, стоимость 5–15 наносекунд.
Начиная с Go 1.19, стандартная библиотека предоставила типобезопасные обёртки: atomic.Int64, atomic.Pointer и низкоуровневую поддержку моделей упорядочения памяти:
memory.OrderRelaxed— атомарность без гарантий синхронизации других переменных;memory.OrderAcqRel— семантика Acquire-Release для барьеров публикации;memory.OrderSequentiallyConsistent— самая строгая последовательная согласованность (используется в Go по умолчанию).
Каналы: дороже, чем кажется
Каналы (chan) — визитная карточка языка Go, воплощающая философию Тони Хоара: «Не общайтесь через разделяемую память, разделяйте память через общение». Однако с микроархитектурной точки зрения канал — это самый сложный и дорогостоящий примитив синхронизации в языке.
Внутренняя структура канала hchan (объявленная в runtime/chan.go) включает:
type hchan struct {
qcount uint // Количество элементов в буфере в данный момент
dataqsiz uint // Емкость кольцевого буфера
buf unsafe.Pointer // Указатель на непрерывный кольцевой буфер в куче
elemsize uint16 // Размер одного элемента в байтах
closed uint32 // Флаг закрытия канала
sendx uint // Индекс кольцевого буфера для записи
recvx uint // Индекс кольцевого буфера для чтения
recvq waitq // Очередь заблокированных горутин-читателей (список sudog)
sendq waitq // Очередь заблокированных горутин-писателей (список sudog)
lock mutex // Внутренний мьютекс, защищающий структуру hchan!
}
Каждая отправка (ch <- x) или прием (<-ch) данных через канал обязана:
- Захватить внутренний замок
hchan.lock(низкоуровневыйruntime.mutex). - Проверить состояние очередей
sendqиrecvq. - При наличии прямого рандеву (небуферизованный канал со спящим партнером) — выполнить прямое копирование памяти из стека отправителя в стек получателя (Zero-buffer memory copy) и пробудить партнера через
goready. - При наличии свободного места в буфере — скопировать данные в кольцевой буфер
buf. - При невозможности операции — аллоцировать или извлечь дескриптор ожидания
sudog, привязать текущую горутину к очередиsendq/recvq, отпустить замокhchan.lockи уснуть вgopark.
Аппаратная стоимость каналов
- Небуферизованный канал (Rendezvous): успешная передача требует захвата блокировки, координации двух горутин и вызова планировщика. Стоимость: 100–300 наносекунд.
- Буферизованный канал (Buffered, без блокировки): доступ к кольцевому буферу под защитой
hchan.lock. Стоимость: 50–150 наносекунд. - Блокирующий канал (Wait/Sleep): парковка горутины, переключение контекста и последующее пробуждение. Стоимость: от 1 до 100+ микросекунд.
Вывод: канал в 3–10 раз дороже обычного sync.Mutex даже на быстром пути! Никогда не используйте каналы для тривиальной защиты внутренних полей структур от гонок данных. Каналы созданы для передачи владения данными и оркестрации потоков управления, а не для синхронизации доступа к разделяемым переменным.
WaitGroup, Once, Cond: специализированные примитивы
sync.WaitGroup
sync.WaitGroup координирует завершение группы параллельных горутин. Под капотом она содержит атомарный 64-битный счетчик (объединяющий число ожидаемых задач и число спящих горутин) и семафор:
- Методы
AddиDone: быстрый атомарный инкремент/декремент счетчика (стоимостьWaitGroup.Done— ~10 наносекунд). - Метод
Wait: если счетчик равен нулю, возврат мгновенен; если счетчик положителен — вызовWaitGroup.Waitпаркует горутину на внутреннем семафоре рантайма.
sync.Once
sync.Once гарантирует, что инициализирующая функция выполнится строго один раз за все время жизни процесса:
- Внутри содержит флаг
atomic.Uint32иsync.Mutex. - Быстрый путь вызова
Once.Do: после того как инициализация завершена, последующие миллионы вызовов выполняют лишь одно атомарное чтение флага с семантикой Acquire, что обходится всего в ~2 наносекунды!
sync.Cond
sync.Cond реализует условную переменную (Condition Variable), позволяя горутинам ждать наступления сложного составного события:
- Работает исключительно в связке с переданным
sync.Locker(обычноsync.Mutex). - Метод
Waitатомарно отпускает мьютекс и усыпляет горутину в очереди ожиданияsync.Cond. - Методы
Signal(пробуждение одной горутины) иBroadcast(широковещательное пробуждение всех горутин) выводят ожидающие задачи из спячки.
Сравнительная таблица стоимости
Сводные результаты измерений аппаратной стоимости примитивов на процессоре x86-64 с частотой 3.0 ГГц при отсутствии конкуренции (Uncontended Fast Path) и в условиях конфликта (Contention Slow Path):
| Операция синхронизации | Стоимость (Fast Path) | Поведение и цена при Contention (Slow Path) |
|---|---|---|
atomic.Load (чтение) |
1–2 нс | Аппаратная когерентность: ~5–10 нс (без блокировок) |
atomic.Store (запись) |
2–5 нс | Инвалидация кэш-линий MESI: ~10–20 нс |
atomic.Add / CAS |
5–15 нс | Spin-цикл CAS: ~15–50 нс |
Mutex.Lock (uncontended) |
10–20 нс | Спиннинг $\to$ futex(FUTEX_WAIT) $\to$ парковка: 1–10+ мкс |
Mutex.Unlock |
5–10 нс | Пробуждение через futex(FUTEX_WAKE): 1–5 мкс |
RWMutex.RLock (uncontended) |
10–20 нс | При наличии ожидающего писателя: парковка 1–10 мкс |
RWMutex.RUnlock |
5–10 нс | Пробуждение писателя на семафоре writerSem |
Channel (buffered, no wait) |
50–150 нс | Ожидание замка hchan.lock, блокировка буфера: 1–100 мкс |
Channel (unbuffered rendezvous) |
100–300 нс | Прямое рандеву, координация двух горутин |
WaitGroup.Done |
~10 нс | Атомарный декремент, пробуждение Wait |
WaitGroup.Wait (waits) |
мгновенно (если 0) | Парковка горутины на семафоре: 1–10 мкс |
Once.Do (cached) |
~2 нс | Первый вызов: захват sync.Mutex + исполнение функции |
Инструменты измерения стоимости синхронизации
Senior-инженер никогда не полагается на догадки — он измеряет задержки примитивов объективными инструментами профилирования:
Mutex profile
Профилировщик мьютексов измеряет время, которое горутины проводят в ожидании освобождения замков sync.Mutex и sync.RWMutex:
- Запуск тестов:
go test -mutexprofile=mutex.out - В боевом приложении: подключение эндпоинта
/debug/pprof/mutexпосле активации сбора сэмплирования черезruntime.SetMutexProfileFraction(1). Детальный разбор представлен в лекции 6. mutex profile.
Block profile
Профилировщик блокировок отслеживает задержки, возникающие при ожидании каналов, мьютексов, таймеров и системных вызовов:
- Запуск тестов:
go test -blockprofile=block.out - В боевом коде: эндпоинт
/debug/pprof/blockс включением сбора черезruntime.SetBlockProfileRate(1). Детальный разбор в лекции 5. block profile.
Execution tracer
Утилита go tool trace визуализирует блокировки на временной шкале: вы можете увидеть, какие именно горутины выстроились в очередь к замку и как долго поток простаивал в ожидании пробуждения.
Бенчмарки
Написание точных микробенчмарков пакета testing с флагами детального замера памяти и многократных прогонов:
go test -bench=. -benchmem -count=10
позволяет с абсолютной достоверностью сравнить производительность sync.Mutex против sync/atomic в вашем конкретном сценарии.
Mechanical Sympathy: как синхронизация влияет на процессор и кэш
Чтобы по-настоящему понять, почему конкурирующий код начинает тормозить, нужно спуститься на уровень кремния:
flowchart LR
classDef process fill:#1e293b,stroke:#38bdf8,stroke-width:1px,color:#f8fafc
classDef success fill:#064e3b,stroke:#34d399,stroke-width:1px,color:#f8fafc
classDef error fill:#7f1d1d,stroke:#f87171,stroke-width:1px,color:#f8fafc
classDef warning fill:#78350f,stroke:#fbbf24,stroke-width:1px,color:#f8fafc
classDef entry fill:#1e3a5f,stroke:#38bdf8,stroke-width:2px,color:#f8fafc
classDef aux fill:#334155,stroke:#64748b,stroke-width:1px,color:#f8fafc
classDef system fill:#312e81,stroke:#a78bfa,stroke-width:1px,color:#f8fafc
classDef data fill:#132d29,stroke:#2dd4bf,stroke-width:1px,color:#f8fafc
classDef network fill:#881337,stroke:#fb7185,stroke-width:1px,color:#f8fafc
classDef accent fill:#1e1b4b,stroke:#818cf8,stroke-width:1px,color:#f8fafc
subgraph Core0 ["Ядро CPU 0"]
C0_L1["Кэш L1d: Линия 64B<br/>(state = Modified)"]
end
subgraph Core1 ["Ядро CPU 1"]
C1_L1["Кэш L1d: Линия 64B<br/>(state = Invalid)"]
end
Core0 <===>|"Шина межъядерной когерентности MESI<br/>(Cache Line Bouncing: задержка 40-100 нс)"| Core1
- Биение кэш-линий (Cache Line Bouncing):
Полеstateмьютекса или переменнаяatomic.Int64физически занимают несколько байт внутри 64-байтовой строки кэша (Cache Line). По протоколу когерентности MESI ядро CPU обязано получить эксклюзивный статусModified, чтобы выполнить операциюLOCK CMPXCHG. При высокой конкуренции 16 или 32 ядра процессора начинают непрерывно отбирать эту строку кэша друг у друга. Строка мечется между кэшами L1d разных ядер через межъядерный интерконнект, увеличивая время доступа к памяти с 1 нс до 60–100 нс на каждую операцию! Если рядом с мьютексом лежат полезные данные структуры, возникает катастрофическое явление False Sharing (8. False sharing). - Аппаратные барьеры памяти (Memory Barriers):
Атомарные инструкции и захват мьютексов вызывают остановку спекулятивного исполнения команд на конвейере CPU и принудительно сбрасывают буфер отложенной записи (Store Buffer) в L1-кэш, чтобы гарантировать упорядоченность доступа к памяти. Это замораживает конвейер инструкций процессора на десятки тактов. - Нагрузка на планировщик ядра Linux:
Массовые блокировки приводят к шторму системных вызововfutex(FUTEX_WAIT)иfutex(FUTEX_WAKE), вынуждая ядро Linux тратить ресурсы на переключение контекста системных потоков.
Когда какой примитив выбирать
Сформулируем золотые правила архитектурного выбора инструментов синхронизации:
- Используйте
sync/atomic: для изолированных числовых счетчиков, флагов состояния, указателей конфигурации (atomic.Pointer) и оптимистичных lock-free циклов. Это самый быстрый примитив, не нагружающий планировщик. - Используйте
sync.Mutex: для защиты критических секций, состоящих из нескольких полей, обновления коллекций (мапы, слайсы), а также при времени удержания замка более 20–50 наносекунд. Мьютекс прост, безопасен и высокоэффективен. - Используйте
sync.RWMutex: исключительно в структурах с редкими записями (< 5%) и преобладающим чтением (> 95%), при условии, что операции чтения длятся сотни наносекунд. - Используйте каналы (
chan): для передачи владения данными между горутинами, реализации конвейеров (Pipelines), диспетчеризации событий и ограничения параллелизма (семафоры). Не используйте каналы взамен обычногоsync.Mutex. - Используйте
sync.WaitGroup: для ожидания завершения параллельных батчей горутин. - Используйте
sync.Once: для ленивой потокобезопасной инициализации глобальных ресурсов (клиенты БД, синглтоны).
Итог
- Стоимость примитивов синхронизации в Go варьируется от 1 наносекунды (атомарное чтение) до десятков микросекунд при глубоком конфликте за ресурсы (Contention).
sync.Mutexидеален для общего применения: на быстром пути он стоит 10–20 нс (1 атомарный CAS), на медленном — задействует активный спиннинг и засыпает наfutex(FUTEX_WAIT). Режим голодания защищает долго ожидающие горутины от starvation.sync.RWMutexобеспечивает параллельное чтение, но на частых записях уступает обычному мьютексу из-за накладных расходов на координацию читателей.sync/atomicоперирует на уровне инструкций кремниевого процессора, являясь самым дешевым инструментом синхронизации простых типов.- Каналы (
chan) — высокоуровневый архитектурный примитив оркестрации. Из-за внутреннего замкаhchan.lockи копирования буферов они значительно дороже мьютексов. - На аппаратном уровне конкуренция за синхронизацию проявляется в виде биения кэш-линий (Cache Line Bouncing), вымывания store-буферов и системных вызовов futex.
- Для выявления узких мест синхронизации применяются
mutex profile,block profile, трассировкаgo tool traceи бенчмарки с ключом-benchmem -count=10.
Теперь, овладев точными метриками стоимости примитивов, мы готовы разрешить главный философский и практический спор сообщества Go — лоб в лоб столкнуть мьютексы и каналы в лекции 6. Mutex vs channel.