---
title: "sync/atomic"
description: "Сервис считает обработанные запросы, чтобы отдавать число в метрики. Счётчик увеличивают тысячи горутин в секунду. Мьютекс справится, но ради одного n++ горутины будут вставать в…"
canonical: https://graphlms.ru/go/book/atomic
course: "Конкурентность Go"
updated: 2026-10-12
lang: ru
publisher: GraphLMS
---

# sync/atomic

> Источник: https://graphlms.ru/go/book/atomic — курс «Конкурентность Go», GraphLMS.

## О чём эта глава

Сервис считает обработанные запросы, чтобы отдавать число в метрики. Счётчик
увеличивают тысячи горутин в секунду. Мьютекс справится, но ради одного `n++` горутины
будут вставать в очередь, а при занятом мьютексе ещё и засыпать и просыпаться через
планировщик. Для такого случая процессор умеет больше: увеличить число в памяти одной
неделимой инструкцией. Доступ к таким инструкциям даёт пакет `sync/atomic`.

В главе разберём, что значит «атомарно», какие гарантии видимости даёт atomic с точки
зрения [модели памяти](/go/book/memory-model), когда atomic быстрее
[мьютекса](/go/book/sync-primitives), а когда нет, как устроен цикл с
`CompareAndSwap` и как атомарно подменить целую структуру данных.

## Перед чтением

Нужны указатели, `WaitGroup`, `Mutex` и различие между гонкой данных и логической гонкой
(race condition) из [главы о модели памяти](/go/book/memory-model). Начните с методов
`Add`, `Load` и `Store` и с вопроса, почему два атомарных вызова подряд не становятся
одной неделимой операцией. Разделы про CAS и `atomic.Pointer` можно перечитать после
первой задачи; для `atomic.Pointer[T]` нужен синтаксис дженериков (квадратные скобки с
типом).

Два слова, которые встретятся дальше. **Опубликовать** данные — сделать полностью
подготовленный объект доступным другим горутинам. **Снимок** (snapshot) — версия данных,
которую после публикации больше не меняют.

## Что такое атомарная операция

Атомарная операция над переменной выполняется **неделимо**: любая другая горутина видит
либо состояние до операции, либо после, и никогда — промежуточное. Нет момента, когда
увеличение «применилось наполовину».

Обычный `n++` неделимым не является. Это три шага: прочитать `n` из памяти, прибавить
единицу, записать обратно. Если две горутины выполнят эти шаги вперемешку, одно
увеличение потеряется. Атомарное `Add` делает все три шага одной операцией процессора,
в которую другие ядра вклиниться не могут.

С Go 1.19 в пакете есть типы-обёртки, и пользоваться стоит ими:

```go
var n atomic.Int64
n.Add(1)                       // атомарно прибавить, возвращает новое значение
v := n.Load()                  // атомарно прочитать
n.Store(10)                    // атомарно записать
old := n.Swap(5)               // записать новое, вернуть прежнее
ok := n.CompareAndSwap(5, 6)   // если сейчас 5 — записать 6 и вернуть true
```

Есть `atomic.Int32`, `Int64`, `Uint32`, `Uint64`, `Uintptr`, `Bool`, обобщённый
`atomic.Pointer[T]` и `atomic.Value` для значения произвольного типа.

Старый стиль — функции над обычными переменными: `atomic.AddInt64(&n, 1)`,
`atomic.LoadInt64(&n)`. Он работает, но у него две проблемы, которые обёртки решают.
Во-первых, ничто не мешает где-то рядом написать обычное `n++` или `x := n`, и это сразу
гонка данных: переменная выглядит как любая другая. У `atomic.Int64` поле спрятано, и
обратиться к нему можно только через методы. Во-вторых, на 32-битных платформах
(386, ARM32) 64-битные атомарные функции требуют, чтобы переменная лежала по адресу,
кратному 8; поле `int64` в середине структуры может оказаться невыровненным, и программа
упадёт с паникой. Тип `atomic.Int64` выравнивается автоматически.

Обёртки, как и мьютекс, **нельзя копировать** после первого использования: у копии будет
своя независимая ячейка. Держите их внутри структуры, которую передают по указателю;
`go vet` предупредит о копировании.

Десять горутин по сто раз вызывают `Add(1)`:

```go play
package main

import (
	"fmt"
	"sync"
	"sync/atomic"
)

func main() {
	var n atomic.Int64
	var wg sync.WaitGroup
	for i := 0; i < 10; i++ {
		wg.Add(1)
		go func() {
			defer wg.Done()
			for j := 0; j < 100; j++ {
				n.Add(1)
			}
		}()
	}
	wg.Wait()
	fmt.Println("итог:", n.Load())
}
```

```text
итог: 1000
```

Обратите внимание на `wg.Wait()` перед чтением. Атомарность защищает каждую операцию по
отдельности, но не говорит, когда закончатся остальные. Без `Wait` `Load` мог бы
выполниться, когда горутины досчитали только до 300, и это было бы корректное, но
бесполезное значение.

## Гарантии видимости

Атомарные операции — ещё и действия синхронизации. Если атомарное чтение увидело
значение, записанное атомарной записью, то запись happens-before чтения. Значит, вместе
со значением атомика читатель видит и всё, что писатель записал **до** атомарной записи.

На этом построена публикация по флагу готовности:

```go
var ready atomic.Bool
var data []byte

// писатель
data = build()        // обычная запись
ready.Store(true)     // публикация

// читатель
if ready.Load() {     // увидели true...
    use(data)         // ...значит, видим и результат build()
}
```

Цепочка: `data = build()` раньше `ready.Store(true)` по порядку строк писателя;
`Store` happens-before `Load`, который увидел `true`; `Load` раньше `use(data)` по
порядку строк читателя. По транзитивности `build()` happens-before `use(data)`.

У гарантии есть граница. Она работает, только если читатель обращается к `data` после
того, как увидел `true`, и только если после публикации `data` никто не меняет. Если
писатель после `Store(true)` продолжит дописывать в `data`, это снова гонка: флаг уже
поднят, а новые записи ни с чем не связаны.

Все атомарные операции в программе ведут себя так, будто выполняются в одном общем
порядке, который согласован с порядком строк каждой горутины (последовательная
согласованность). В других языках бывают «ослабленные» атомики с более слабыми
гарантиями; в Go их нет.

## atomic или мьютекс

Граница проходит по тому, сколько данных должна охватить операция.

**atomic** подходит для одной переменной: счётчик, флаг, указатель на неизменяемый
объект.

**Мьютекс** нужен, когда операция затрагивает несколько переменных или состоит из
нескольких шагов: «проверить остаток и согласованно обновить три поля». Набор
атомарных вызовов такой участок неделимым не делает. Бывают алгоритмы на CAS и
неизменяемых снимках, которые обходятся без мьютекса, но мьютекс почти всегда проще
написать и проверить.

По скорости всё решает **конкуренция** (contention): насколько часто горутины на разных
ядрах обращаются к одной и той же ячейке.

- При низкой конкуренции atomic заметно дешевле мьютекса: никто не засыпает и не
  просыпается.
- При высокой конкуренции за одну ячейку обе схемы упираются в одно и то же. Чтобы
  изменить значение, ядро должно получить строку кэша с этой ячейкой в своё
  исключительное владение, и строка начинает непрерывно перебрасываться между ядрами.
  В таком случае помогает **шардирование**: несколько независимых счётчиков (скажем, 32),
  каждая горутина пишет в «свой» по хешу или номеру, а при чтении значения суммируются.
  Чтобы соседние счётчики не делили одну строку кэша (64 байта на большинстве
  процессоров), между ними иногда вставляют пустые поля-прокладки.

Простейший счётчик на atomic:

```go
type Counter struct{ n atomic.Int64 }

func (c *Counter) Inc()         { c.n.Add(1) }
func (c *Counter) Value() int64 { return c.n.Load() }
```

Практическое правило: одиночный счётчик или флаг — atomic; согласованная группа данных —
мьютекс; очень горячая ячейка — сначала измерить (`go test -bench` с `-cpu=1,4,8`), потом
думать о шардировании.

## CAS: прочитать, вычислить, записать, если никто не помешал

`CompareAndSwap(old, new)` записывает `new`, только если в ячейке сейчас `old`, и
сообщает, получилось ли. Из этого строят «условные» атомарные изменения: прочитали
значение, вычислили новое, попытались записать; если кто-то успел изменить ячейку,
повторяем.

```go play
package main

import (
	"fmt"
	"sync"
	"sync/atomic"
)

// addIfPositive прибавляет d, только если текущее значение больше нуля.
func addIfPositive(n *atomic.Int64, d int64) bool {
	for {
		old := n.Load()
		if old <= 0 {
			return false // условие не выполнено
		}
		if n.CompareAndSwap(old, old+d) {
			return true // никто не вмешался между Load и CAS
		}
		// значение изменили между Load и CAS — пробуем снова
	}
}

func main() {
	var n atomic.Int64
	n.Store(100)

	var wg sync.WaitGroup
	for i := 0; i < 50; i++ {
		wg.Add(1)
		go func() {
			defer wg.Done()
			addIfPositive(&n, 1)
		}()
	}
	wg.Wait()
	fmt.Println("итог:", n.Load())
}
```

```text
итог: 150
```

Разберём одну неудачную попытку. Горутина A прочитала `old = 120`. Пока она вычисляла
`121`, горутина B успешно записала `121`. Теперь A вызывает `CompareAndSwap(120, 121)`:
в ячейке не `120`, а `121`, запись не происходит, метод возвращает `false`, и A идёт на
второй круг, читая уже `121`. Ни одно увеличение не теряется.

Цена такой схемы — повторные попытки. При высокой конкуренции горутины много раз
читают, вычисляют и проигрывают, тратя процессор.

Вторая ловушка — **проблема ABA**. Между вашим `Load` и `CAS` значение могло смениться с
A на B и обратно на A. CAS увидит A и решит, что ничего не изменилось. Для счётчиков,
которые только растут, это не страшно. Для указателей на объекты, которые
переиспользуются (например, узлы самописного lock-free стека), это реальная ошибка:
указатель тот же, а объект за ним уже другой.

## atomic.Pointer и atomic.Value: подмена целого снимка

Конфигурацию сервиса читают на каждом запросе, а меняют раз в несколько минут.
Мьютекс на каждом чтении здесь лишний. Вместо этого писатель собирает новый объект
конфигурации целиком и атомарно подменяет указатель на него. Читатели всегда получают
какую-то одну целую версию: старую или новую.

```go
var cfg atomic.Pointer[Config]

func Reload(c *Config) { cfg.Store(c) }        // публикуем новую версию целиком
func Current() *Config { return cfg.Load() }   // берём текущий снимок
```

Этот приём называют copy-on-write («копирование при записи»): изменение — это всегда
новая копия, а не правка старой. Читатель, взявший старый снимок, спокойно дорабатывает
с ним; следующий читатель получит новый. Главное условие: опубликованный объект никто
не меняет. Атомарна только подмена указателя, а не поля объекта за ним.

Запускаемый пример использует `atomic.Value`: в браузерной песочнице нет обобщённого
`atomic.Pointer[T]`. Смысл тот же, только при чтении нужно приведение типа:

```go play
package main

import (
	"fmt"
	"sync/atomic"
)

type Config struct {
	Version int
	Workers int
}

func main() {
	var cfg atomic.Value // в рабочем коде удобнее atomic.Pointer[Config]
	cfg.Store(&Config{Version: 1, Workers: 4})

	snap := cfg.Load().(*Config) // читатель взял снимок до обновления

	cfg.Store(&Config{Version: 2, Workers: 8}) // писатель публикует новую версию

	now := cfg.Load().(*Config)
	fmt.Printf("старый снимок: v%d, %d workers\n", snap.Version, snap.Workers)
	fmt.Printf("текущий конфиг: v%d, %d workers\n", now.Version, now.Workers)
}
```

```text
старый снимок: v1, 4 workers
текущий конфиг: v2, 8 workers
```

Увидеть «половину» обновления (новый `Version` со старым `Workers`) невозможно:
подменяется указатель целиком, одной атомарной записью.

## Частые ошибки

- **Атомарный и обычный доступ к одной переменной.** Если переменную меняют через
  atomic, все обращения к ней должны быть атомарными. Одно обычное чтение рядом с
  `atomic.AddInt64` — гонка данных. Типы-обёртки не дают совершить эту ошибку.
- **Два атомарных вызова считают одной операцией.** `Load`, потом `Store` — две
  неделимые операции, между которыми может вклиниться другая горутина. Для «проверить и
  обновить» нужен CAS-цикл или мьютекс.
- **Копирование обёртки.** `Counter`, переданный по значению, — это другой счётчик.
- **Несколько atomic-полей вместо мьютекса.** Каждое поле по отдельности атомарно, но
  согласованности между полями нет: читатель может увидеть новое значение одного поля
  и старое значение другого.

## Мысленная модель

atomic даёт две вещи: неделимость одной операции над одной переменной и
happens-before от записи к чтению, которое её увидело. Через вторую можно публиковать и
другие данные, если после публикации их не меняют. Как только операция затрагивает
несколько переменных или складывается из «прочитать → решить → записать», остаётся два
варианта: CAS-цикл или мьютекс.

## Попробуйте сами

Замените в счётчике `n.Add(1)` на два вызова: `old := n.Load()` и `n.Store(old + 1)`.
Для двух горутин и начального значения 0 выпишите порядок шагов, при котором итог будет
1. Найдёт ли эту ошибку `go test -race`? Как сохранить требование «каждый вызов
увеличивает ровно на один»?

### Проверка

A читает 0; B читает 0; A записывает 1; B записывает 1. Итог 1, одно увеличение потеряно.

Race detector эту ошибку не найдёт: все обращения атомарные, гонки данных нет. Это
логическая гонка (race condition): неделимыми оказались отдельные шаги, а не вся
операция. Исправление — `n.Add(1)`. Если новое значение зависит от проверки старого,
нужен CAS-цикл, как в `addIfPositive`, или мьютекс на всю операцию.

## Что дальше

Следующая глава, [context](/go/book/context), возвращается к каналам: она показывает,
как одним сигналом остановить целое дерево горутин.

В задачах курса atomic нужен в [блоке 2](/go/topics/2) (задача 05: счётчик на мьютексе и на atomic) и в
[блоке 5](/go/topics/5) (лимитеры, счётчики токенов). Задачи проверяют не столько знание API, сколько
обоснованный выбор: почему здесь atomic, а здесь мьютекс.
