Конкуренция в Go
Теория: Атомарные операции
В конкурентных программах часто встречаются одинаковые задачи: увеличить счетчик, установить флаг, прочитать актуальную конфигурацию. Эти операции кажутся простыми, но выполняются из разных горутин одновременно. Самый очевидный способ защитить общую переменную — обернуть доступ к ней в sync.Mutex.
Однако такой подход быстро начинает тормозить систему. Один мьютекс блокирует доступ ко всей структуре сразу: несколько горутин ждут друг друга даже тогда, когда им нужно выполнить всего одну элементарную операцию — например, прибавить единицу или проверить значение флага. Под нагрузкой очередь на Lock() растет, и задержки увеличиваются без реальной необходимости.
Такая ситуация поднимает естественный вопрос: нужно ли держать полноценную блокировку ради простейших операций над одним словом памяти?
Для этих случаев в Go предусмотрены атомарные операции. Они дают возможность безопасно изменять отдельные значения без мьютексов — быстро, неделимо и с корректным порядком видимости между горутинами. Атомики не заменяют мьютексы полностью, но отлично подходят там, где критическая секция состоит из одной простой операции.
Пакет sync/atomic
Пакет sync/atomic дает примитивы для работы с отдельными машинными словами: целыми числами, флагами, указателями. Операции выполняются атомарно — либо целиком, либо никак. Процессор не может «вклинить» чужую запись в середину, а другие потоки видят изменения в правильном порядке.
Важно ограничение: атомики работают только с простыми значениями фиксированного размера. При необходимости, часто используется сторонняя библиотека
https://github.com/uber-go/atomic. Это не «легкий мьютекс» для любых структур. Если нужно обновлять сразу несколько полей согласованно или изменять сложный объект, без Mutex или каналов не обойтись.
В пакете есть две группы API:
- функции вроде
atomic.AddInt64,atomic.LoadUint32,atomic.StorePointer,CompareAndSwap…, которые принимают адрес обычной переменной; - типобезопасные обертки (
atomic.Int64,atomic.Boolи т. д. в новых версиях Go).
Общее правило одно: если переменная хоть раз читается или пишется через atomic, все обращения к ней должны быть только атомарными. Нельзя частично использовать атомики, частично — обычный x++ и прямое чтение
atomic.AddInt64, atomic.Load, atomic.Store
Самые частые сценарии — счетчики и флаги. Для них достаточно трех операций: добавить, прочитать и записать.
Счетчик через atomic.AddInt64
Вместо mu.Lock(), counter++, mu.Unlock() можно использовать атомарный инкремент:
Здесь:
atomic.AddInt64(&counter, 1)атомарно читает текущее значение, прибавляет единицу и записывает обратно;atomic.LoadInt64(&counter)безопасно читает итог.
Ни один Mutex не нужен: все происходит на уровне инструкций процессора.
Флаг остановки через Load/Store
Второй популярный пример — флаг «пора остановиться». Его удобно хранить в uint32 и читать без блокировок:
- Воркер периодически вызывает
atomic.LoadUint32(&stop)и проверяет, не стал ли флаг равен1. - Основная горутина один раз делает
atomic.StoreUint32(&stop, 1), и все воркеры гарантированно увидят это изменение.
Такая схема дешевле, чем мьютекс, потому что ничто не блокируется: читатели просто смотрят на значение, а запись — одна.
Сравнение и обмен (CompareAndSwap)
Иногда важно «выиграть гонку» за право выполнить инициализацию или установить значение только если его еще никто не трогал. Для этого есть операции CompareAndSwap (CAS):
CAS читает текущее значение, сравнивает его с ожидаемым и только тогда записывает новое. Все это — одна неделимая операция.
На CAS строят неблокирующие структуры, одноразовую инициализацию и различные «один победитель — остальные ждут» механики. Но для базового курса достаточно помнить: это способ сделать «записать, только если еще ноль».
Когда атомарные операции лучше мьютексов
Атомики не «вытесняют» мьютексы, они решают другую задачу. Есть ситуации, где атомарная операция — идеальный инструмент, а есть — где она только усложнит жизнь. Любой мьютекс всегда можно заменить на atomic. И atomic будет быстрее (под капотом мьютекса - атомик, мьютекс просто обертка над ним)
Когда атомики уместны:
-
Счетчики запросов и метрик. Нужно быстро и безопасно делать
+1из множества горутин. Здесьatomic.Add…проще и дешевле, чемMutex. -
Флаги и простые статусы. Примеры: «остановить обработку», «конфиг загружен», «инициализация завершена». Достаточно
LoadиStoreдляuint32илиatomic.Bool. -
Указатель на актуальный снимок данных. Часто конфиг меняется редко, а читается постоянно. Удобно хранить его в
atomic.Valueили атомарном указателе: писатель публикует новую структуру целиком, читатели читают ссылку без блокировок.
В этих сценариях критическая секция — одна операция над словом памяти. Мьютекс будет лишь добавлять накладные расходы: очереди ожидания, переключение контекстов, лишние блокировки.
Когда атомики не подходят:
- нужно обновлять несколько полей сразу и гарантировать, что читатель увидит их в согласованном состоянии;
- важно выполнить цепочку действий как одну транзакцию: прочитал, проверил условие, обновил структуру;
- логика сложная, и без мьютекса код превращается в набор
CAS-циклов и флагов, которые трудно читать и сопровождать.
В таких случаях лучше использовать sync.Mutex или sync.RWMutex. Они чуть тяжелее, но яснее выражают идею «этот блок кода выполняет только один поток».
Наконец, несколько практических правил безопасности:
- Если переменная хоть раз трогается через
atomic, все чтения и записи должны быть атомарными. Никаких «иногда простоx = 1, иногдаStore». - Структуры с атомарными полями не копируют после инициализации: копия создает второй адрес, и часть горутин начнет писать в один экземпляр, часть — в другой.
- Атомики — не замена дизайну. Если код с ними становится трудночитаемым, почти всегда проще сделать один
Mutexи четко обозначить границу критической секц


