Две горутины обновляют общий счётчик: все операции выполняются через атомарные примитивы, но итоговая логика всё равно может быть неверной. Как определить, что именно гарантирует атомарность в таком случае?
Атомарность гарантирует неделимость отдельной операции над общей переменной и необходимую синхронизацию между такими операциями. Она устраняет гонку данных, если все обращения к переменной выполняются атомарно или защищены другим механизмом синхронизации, но не делает атомарной составную последовательность вроде чтения, проверки и последующего изменения.
Поэтому программа может не иметь data race и всё равно содержать логическую ошибку: два потока независимо прочитали одно значение и приняли решение на его основе.
Конкурентные программы часто должны обновлять небольшие общие значения без полной блокировки. Для этого в Go существует пакет sync/atomic, а race detector умеет учитывать атомарные операции при анализе доступа к памяти.
Исходная проблема состояла в необходимости разделять два свойства: безопасность доступа к памяти и корректность алгоритма. Атомарные примитивы решают первое для конкретных операций, но не заменяют проектирование критической секции или атомарного протокола.
Обычная запись и чтение общей переменной из разных горутин без синхронизации создают гонку данных. Результат зависит от расписания, а race detector обычно сообщает о конфликтующих доступах.
Атомарное увеличение счётчика безопасно на уровне памяти, но проверка условия и изменение значения могут оставаться разными операциями. Например, два worker-а могут одновременно увидеть, что лимит ещё не достигнут, и оба увеличить счётчик сверх лимита.
Атомарная операция выполняется как единое неделимое действие с точки зрения других горутин. Для счётчика это означает, что два вызова увеличения не потеряют изменения: каждый работает с актуальным значением согласно правилам памяти, заданным атомарным API.
Здесь и изменение, и чтение счётчика атомарны, поэтому доступ к n не образует гонку данных. Ожидаемый результат — два, но это свойство конкретной операции Add, а не гарантия корректности любой последовательности действий.
Если алгоритм требует проверки условия с последующим изменением, нужны Compare-And-Swap, mutex или другой механизм, объединяющий эти действия в один защищённый протокол. Нельзя смешивать атомарный доступ к переменной с обычным чтением или записью: одного атомарного обращения недостаточно, если другое обращение не синхронизировано.
Следует также отличать отсутствие data race от корректности результата. Race detector может не сообщить об ошибке в алгоритме, который использует атомарные операции, но допускает неверный порядок действий или нарушает бизнес-инвариант.
Сервис ограничивал число одновременно принятых задач. Разработчик заменил mutex на атомарный счётчик: сначала загружал текущее значение, затем отдельно проверял лимит, после чего выполнял атомарное увеличение. Тесты и race detector проходили, однако под нагрузкой число принятых задач иногда превышало лимит.
Вариант с одним атомарным счётчиком был быстрым и простым, но не защищал составную операцию. Mutex гарантировал корректность, но создавал избыточную сериализацию при высокой конкуренции. В итоге применили CAS-цикл: значение увеличивалось только при успешной проверке прежнего значения; при конфликте операция повторялась.
Так сохранилась неблокирующая реализация, а проверка лимита и изменение счётчика стали логически единым действием. Если бы операция включала несколько связанных полей или сложные побочные эффекты, более понятным и безопасным выбором был бы mutex.
Вопрос: Достаточно ли атомарного увеличения, чтобы безопасно реализовать проверку лимита?
Ответ: Нет. Увеличение отдельного счётчика безопасно, но последовательность загрузки значения, сравнения с лимитом и увеличения не является одной атомарной операцией. Для неё нужен CAS-цикл или mutex.
Вопрос: Можно ли атомарно записывать переменную, а читать её обычным способом?
Ответ: Нет, если эти обращения выполняются конкурентно. Все доступы к одной общей переменной должны быть согласованы: либо каждый доступ атомарен, либо каждый находится под одной подходящей синхронизацией. Смешивание атомарного и обычного доступа может снова создать гонку данных.
Вопрос: Если race detector ничего не сообщает при использовании атомарных операций, гарантирует ли это правильность алгоритма?
Ответ: Нет. Detector проверяет прежде всего конкурентный доступ к памяти и учитывает корректно применённые примитивы синхронизации. Он не доказывает, что соблюдены бизнес-инварианты, что операция должна быть составной или что выбран правильный порядок действий.