Атомарная операция
Атомарная операция — это последовательность действий (операций) над данными, которая выполняется как единое, неделимое целое. Ключевое свойство атомарной операции заключается в том, что она либо выполняется полностью и успешно, либо не выполняется вовсе, не оставляя после себя промежуточных состояний. В контексте параллельных вычислений и многопоточности атомарность гарантирует, что никакой другой поток или процесс не сможет наблюдать частичный результат выполнения этой операции, что предотвращает возникновение состояний гонки.
История и происхождение понятия
Понятие атомарности восходит к теории баз данных и транзакций, сформулированной в 1970-х годах. Одним из основоположников реляционной модели данных, Эдгаром Коддом, были определены правила целостности, включающие требование неделимости транзакций. В 1983 году группа исследователей во главе с Тео Хардером и Андреасом Ройтером формализовала свойства ACID (Atomicity, Consistency, Isolation, Durability — атомарность, согласованность, изоляция, долговечность), где атомарность стоит на первом месте.
С развитием многопроцессорных систем и многопоточного программирования в 1980–1990-х годах понятие атомарности перешло в область аппаратного обеспечения. Процессоры начали поддерживать специальные инструкции, позволяющие выполнять операции чтения-модификации-записи без прерывания. Первые такие инструкции, например, Test-and-Set (проверка и установка) и Compare-and-Swap (сравнение с обменом), появились в архитектурах IBM System/370 и DEC VAX.
Свойства атомарной операции
Атомарная операция обладает тремя фундаментальными свойствами:
- Неделимость (Indivisibility): Операция не может быть разделена на более мелкие шаги, видимые для других потоков. С точки зрения внешнего наблюдателя, она происходит мгновенно.
- Линеаризуемость (Linearizability): Результат операции становится видимым для всех остальных потоков только после её полного завершения. Это гарантирует, что порядок выполнения атомарных операций в системе является строгим и последовательным.
- Отсутствие частичного эффекта (All-or-Nothing): Если операция прерывается (например, из-за сбоя питания), система откатывается к состоянию, предшествовавшему началу операции. Никакие промежуточные изменения не сохраняются.
Классификация атомарных операций
Атомарные операции можно классифицировать по уровню реализации и по типу выполняемого действия.
По уровню реализации
- Аппаратные (Hardware Atomic Operations): Реализуются на уровне инструкций центрального процессора (ЦП). Примеры:
XCHG(обмен) в архитектуре x86,LDREX/STREX(эксклюзивная загрузка/сохранение) в ARM,CAS(Compare-and-Swap) в большинстве современных процессоров. Эти инструкции гарантируют атомарность на уровне кэш-линии или шины памяти. - Программные (Software Atomic Operations): Реализуются на уровне операционной системы или языка программирования с использованием блокировок, семафоров или мьютексов. Хотя сами по себе блокировки не являются атомарными, они обеспечивают атомарность критических секций кода. Примеры:
std::atomicв C++,java.util.concurrent.atomicв Java,Interlockedв .NET. - Транзакционные (Transactional Operations): Реализуются в системах управления базами данных (СУБД) и некоторых языках программирования (например, Software Transactional Memory). В этом случае атомарность применяется к целой группе операций, объединённых в транзакцию.
По типу действия
- Чтение-модификация-запись (Read-Modify-Write, RMW): Операция, которая читает значение из памяти, изменяет его и записывает обратно. Примеры:
Fetch-and-Add(увеличить значение на 1),Test-and-Set(установить флаг в 1 и вернуть старое значение),Compare-and-Swap(сравнить значение с ожидаемым и, если совпадает, заменить новым). - Атомарная загрузка и сохранение (Atomic Load/Store): Простейшие операции, гарантирующие, что чтение или запись одного машинного слова (обычно 4 или 8 байт) происходит без прерывания. На современных процессорах выровненные обращения к памяти обычно атомарны по умолчанию.
- Атомарные флаги (Atomic Flags): Булевы переменные, поддерживающие атомарные операции установки и сброса. Часто используются для реализации спин-блокировок.
Применение атомарных операций
Атомарные операции являются фундаментом для построения надёжных и производительных параллельных систем.
В многопоточном программировании
- Счётчики и счётчики ссылок: Атомарное увеличение и уменьшение счётчиков (например,
fetch_add) используется для подсчёта количества активных потоков или ссылок на объект (std::shared_ptr в C++). - Спин-блокировки (Spinlocks): Реализуются с помощью атомарных операций
Test-and-SetилиCompare-and-Swap. Поток в цикле пытается атомарно захватить флаг блокировки, пока это не удастся. - Безблокировочные структуры данных (Lock-Free Data Structures): Стеки, очереди и хеш-таблицы, которые не используют блокировки, а опираются исключительно на атомарные операции (например,
CAS) для обеспечения корректности. Это позволяет избежать проблем, связанных с блокировками, таких как взаимные блокировки (deadlocks) и инверсия приоритетов. - Барьеры памяти (Memory Barriers): Атомарные операции часто используются в сочетании с барьерами памяти для обеспечения видимости изменений между потоками. Например,
std::memory_order_releaseиstd::memory_order_acquireв C++.
В операционных системах
- Планировщик задач: Атомарные операции используются для безопасного изменения очередей готовых к выполнению потоков.
- Системные вызовы: Многие системные вызовы, такие как создание процессов или работа с файловыми дескрипторами, внутренне используют атомарные инструкции для синхронизации доступа к структурам ядра.
В базах данных и распределённых системах
- Транзакции ACID: Атомарность транзакций гарантирует, что либо все операции внутри транзакции будут применены, либо ни одна. Это реализуется с помощью журналов упреждающей записи (Write-Ahead Logging, WAL) и механизмов отката (rollback).
- Распределённые блокировки: В системах вроде ZooKeeper или etcd атомарные операции (например,
createс флагомEPHEMERAL) используются для реализации распределённых мьютексов. - Консенсусные алгоритмы: Алгоритмы вроде Paxos и Raft требуют атомарной записи в журнал на большинстве узлов для достижения консенсуса.
Примеры в языках программирования
C++
В C++ атомарные операции предоставляются шаблоном std::atomic<T> (начиная с C++11). Пример атомарного счётчика:
```cpp
include <atomic>
std::atomic<int> counter(0);
void increment() { counter.fetch_add(1, std::memory_order_relaxed); // Атомарное увеличение } ```
Java
В Java атомарные классы находятся в пакете java.util.concurrent.atomic. Пример:
```java import java.util.concurrent.atomic.AtomicInteger; AtomicInteger counter = new AtomicInteger(0);
public void increment() { counter.incrementAndGet(); // Атомарное увеличение и возврат нового значения } ```
C# ( .NET )
В C# атомарные операции предоставляются классом Interlocked:
```csharp using System.Threading; int counter = 0;
void Increment() { Interlocked.Increment(ref counter); // Атомарное увеличение } ```
Критика и ограничения
Несмотря на свою полезность, атомарные операции имеют ряд ограничений:
- Производительность: Атомарные операции на аппаратном уровне могут быть значительно медленнее обычных операций, так как они требуют блокировки шины памяти или кэш-линии, что приводит к инвалидации кэша у других ядер процессора. Это может снизить общую производительность системы при интенсивном использовании.
- Сложность отладки: Ошибки, связанные с неправильным использованием атомарных операций (например, неправильный выбор порядка памяти), могут приводить к трудно воспроизводимым состояниям гонки.
- Ограниченный набор операций: Атомарные операции поддерживают только простые типы данных (целые числа, указатели, флаги). Для сложных структур данных требуются либо блокировки, либо безблокировочные алгоритмы, которые сложны в реализации.
- Проблема ABA: При использовании операции
Compare-and-Swapможет возникнуть ситуация, когда значение переменной меняется с A на B, а затем обратно на A, и операция CAS не замечает этого изменения, считая, что ничего не произошло. Для решения этой проблемы используются версии CAS с тегами (например,double-width CAS) или счётчики.
Источники
- Морис Херлихи, Ним Шавит. «Искусство многопроцессорного программирования». — ДМК Пресс, 2014.
- Эндрю Таненбаум, Херберт Бос. «Современные операционные системы». — 4-е изд. — Питер, 2015.
- Брайан Гетц и др. «Java Concurrency on Practice». — Addison-Wesley, 2006.
- Пол Дж. Лейк. «C++ Concurrency in Action». — 2nd ed. — Manning Publications, 2019.
- Стандарт ISO/IEC 14882:2020 (C++20), раздел 33.5 «Atomic operations library».
- Тео Хардер, Андреас Ройтер. «Principles of Transaction-Oriented Database Recovery». — ACM Computing Surveys, Vol. 15, No. 4, 1983.
BFOmetr — база данных и аналитика по компаниям России.
На главную BFOmetr →