std::atomic은 한 변수에 대한 연산을 원자적으로 만들어 데이터 레이스(data race)를 피하게 하는 C++ 타입입니다.
이 글은 cppreference의 atomic·memory_order 문서를 2026-09-01 기준으로 정리한 일반 설명이며, 표준 버전과 구현에 따라 세부 동작은 달라질 수 있습니다.
std::atomic은 무엇인가?
한 줄 답: 이 타입의 각 인스턴스와 완전 특수화는 원자 타입을 정의하며, 같은 객체를 한 스레드가 쓰고 다른 스레드가 읽어도 해당 접근의 동작은 정의됩니다.
<atomic> 헤더의 C++11 템플릿은 한 객체에 대한 연산을 원자 연산으로 표현합니다. 한 평가가 메모리 위치를 수정하고 다른 평가가 같은 위치를 읽거나 수정할 때, 둘 중 하나 이상이 비원자 연산이고 두 평가 사이에 happens-before 관계도 없으면 데이터 레이스가 발생하여 프로그램의 동작이 정의되지 않습니다.
원자 연산은 쓰기인 store, 읽기인 load, 읽은 뒤 쓰는 read-modify-write (RMW)의 세 종류로 나뉩니다. read-modify-write는 읽기와 쓰기를 여러 개의 분리된 연산으로 처리하는 것이 아니라 하나의 원자 연산으로 처리합니다. 원자 객체에 대한 접근은 memory_order에 따라 스레드 간 동기화를 만들고 비원자 메모리 접근의 순서를 정할 수도 있습니다.
기본 템플릿은 TriviallyCopyable이고 복사·이동 생성과 대입이 가능한 타입을 요구하며, cv 한정 타입에는 사용할 수 없습니다. 이 타입의 객체 자체는 복사할 수도 이동할 수도 없습니다. 포인터와 정수 특수화에는 fetch_add·fetch_sub 같은 추가 RMW 연산이 있으며, exchange, compare_exchange_weak, compare_exchange_strong도 제공됩니다.
C++20에는 wait, notify_one, notify_all 멤버도 있지만, 이 글의 주 경로는 C++11의 load와 store입니다.
뮤텍스와 무엇이 다른가?
한 줄 답: 이 객체는 변수 하나의 연산을 원자적으로 다루고, 뮤텍스는 여러 연산을 포함할 수 있는 임계 구역(critical section)을 잠그는 도구입니다.
따라서 한 변수의 값을 한 번 읽거나 쓰는 연산과 여러 변수를 포함한 여러 단계를 하나의 일관된 임계 구역으로 묶는 문제는 구분해야 합니다. 이 객체를 사용한다고 해서 여러 변수에 걸친 상태 변경 전체가 하나의 원자 연산이 되는 것은 아닙니다.
메모리 순서 관점에서 뮤텍스의 lock()은 acquire 연산이고 unlock()은 release 연산입니다. 이 관계를 통해 잠금 전후의 메모리 접근 순서가 연결됩니다.
is_lock_free()는 해당 원자 객체가 lock-free인지 확인합니다. 모든 타입에 lock-free가 보장되는 것은 아니며, 구현에 따라 이 객체의 연산이 내부 잠금을 사용할 수도 있습니다.
표준 volatile 접근은 원자적이지 않으며, 동시 읽기·쓰기는 데이터 레이스입니다.
뮤텍스와 세마포어는 임계 구역의 잠금 개념을 별도로 다루는 참고 글입니다.
load와 store는 어떻게 쓰나?
한 줄 답: store는 값을 원자적으로 바꾸고 load는 현재 값을 원자적으로 읽으며, 둘 다 인자를 생략하면 memory_order_seq_cst를 사용합니다.
먼저 <atomic>을 포함하고 초기값을 지정한 뒤, 이름 있는 store와 load로 쓰기와 읽기를 표현합니다. 다음 C++11 예시에서는 두 멤버의 기본 메모리 순서를 그대로 사용합니다.
#include <atomic>
std::atomic<int> flag{0};
void publisher()
{
flag.store(1);
}
int observer()
{
return flag.load();
}
store의 형식은 void store(T desired, std::memory_order order = std::memory_order_seq_cst) noexcept이고, load의 형식은 T load(std::memory_order order = std::memory_order_seq_cst) const noexcept입니다. 전자는 현재 값을 desired로 원자적으로 바꾸고, 후자는 현재 값을 원자적으로 읽어 반환합니다.
store에 std::memory_order_consume, std::memory_order_acquire, std::memory_order_acq_rel을 전달하면 동작이 정의되지 않습니다. load에 std::memory_order_release 또는 std::memory_order_acq_rel을 전달해도 동작이 정의되지 않으므로, 각 멤버에 허용되는 순서 인자를 구분해야 합니다.
암시적 변환 연산자도 원자 객체의 값을 읽지만, 의도를 드러내는 설명과 코드에서는 이름 있는 load가 명확합니다. 정수 특수화의 fetch_add 등과 exchange, compare_exchange_weak, compare_exchange_strong은 같은 객체에서 사용하는 RMW 멤버라는 점만 기억하면 됩니다.
C++ 기술면접 질문 10가지는 공유 변수의 원자 연산과 별도로 C++ 핵심 개념 범위를 점검하는 참고 글입니다.
memory_order는 왜 고르나?
한 줄 답: memory_order는 원자성 외에 원자 연산 주위의 일반 메모리 접근까지 어떤 순서 제약을 둘지 지정합니다.
라이브러리의 원자 연산은 기본적으로 memory_order_seq_cst를 사용하며, 이 순서는 모든 스레드가 원자 객체의 수정 사항을 같은 순서로 관측하는 단일 전체 순서(single total order)를 추가합니다. 처음에는 기본값으로 시작하고, 원자성 외에 필요한 동기화 관계가 분명할 때만 더 약한 순서를 추가 인자로 고르는 흐름이 적절합니다.
| 이름 | 짧은 의미 |
|---|---|
memory_order_relaxed |
다른 읽기·쓰기에 대한 동기화나 순서 제약 없이 해당 연산의 원자성만 보장합니다. |
memory_order_consume |
consume load에 해당하며, 문서에서는 C++26에 deprecated되고 acquire와 같은 효과로 설명합니다. |
memory_order_acquire |
load에 적용하는 acquire 순서이며, 같은 객체의 release store가 기록한 값을 읽을 때 앞선 쓰기를 관측하는 관계를 만듭니다. |
memory_order_release |
store에 적용하는 release 순서이며, 현재 스레드의 앞선 읽기·쓰기를 같은 객체를 acquire한 다른 스레드에 보이게 합니다. |
memory_order_acq_rel |
RMW 연산에 acquire와 release를 함께 적용합니다. |
memory_order_seq_cst |
load는 acquire, store는 release, RMW는 둘 다로 동작하며 모든 스레드가 모든 수정 사항을 같은 순서로 관측하는 단일 전체 순서를 추가합니다. |
카운터처럼 이 변수의 원자성만 필요하고 다른 메모리 접근을 스레드 사이에 전달할 필요가 없다면 memory_order_relaxed를 고려할 수 있습니다. 이 순서는 다른 읽기·쓰기에 대한 동기화나 순서를 추가하지 않으므로, payload를 공개하거나 여러 접근의 순서를 전달해야 하는 경우에는 충분하지 않을 수 있습니다.
플래그와 비원자 payload를 함께 전달할 때는 release store와 acquire load를 짝지을 수 있습니다. acquire load가 release store가 기록한 값을 관측하면, 그 store보다 앞선 payload 쓰기도 관측할 수 있습니다.
#include <atomic>
int payload = 0;
std::atomic<int> flag{0};
void publisher()
{
payload = 42;
flag.store(1, std::memory_order_release);
}
int observer()
{
while (flag.load(std::memory_order_acquire) == 0) {
}
return payload;
}
이 예시의 핵심은 flag의 원자성만이 아니라 release와 acquire가 같은 원자 객체를 통해 연결된다는 점입니다. 실제 프로그램에서는 이 관계가 성립하는 경로와 payload의 다른 쓰기가 없는지도 함께 확인해야 합니다.
FAQ
한 줄 답: 공유 변수의 원자적 읽기·쓰기는 이 타입의 load와 store에서 시작하고, 여러 단계의 임계 구역과 메모리 순서 요구는 별도로 구분합니다.
C++에서 공유 변수를 원자적으로 읽고 쓰려면 무엇을 사용합니까? C++11의 이 타입으로 해당 변수의 연산을 원자적으로 만듭니다. 한 객체의 읽기와 쓰기를 원자적으로 처리해야 하는지, 여러 연산을 함께 보호해야 하는지는 별도로 판단합니다.
load와 store의 기본 memory_order는 무엇입니까? memory_order_seq_cst입니다. 두 연산에서 순서 인자를 생략하면 이 기본값이 사용됩니다.
뮤텍스 대신 이 객체를 사용하면 됩니까? 한 변수에 대한 개별 원자 연산이면 이 객체를 검토할 수 있지만, 여러 연산을 하나의 임계 구역으로 묶어야 한다면 보호 범위를 비교해야 합니다.
volatile은 원자적입니까? 아닙니다. 표준 volatile 접근은 원자적이지 않으며 동시 읽기·쓰기는 데이터 레이스입니다.
출처
한 줄 답: 본문은 2026-09-01 기준으로 확인한 cppreference의 atomic, memory_order, load, store 문서만 근거로 사용합니다.
- std::atomic — 원자 타입,
store·load·RMW 연산, 타입 제약과is_lock_free를 확인할 수 있습니다. - std::memory_order — 데이터 레이스, 메모리 순서 열거자, 기본
seq_cst, acquire·release 관계를 설명합니다. - atomic::store —
store시그니처, 기본 순서와 허용하지 않는 순서 인자를 설명합니다. - atomic::load —
load시그니처, 기본 순서와 허용하지 않는 순서 인자를 설명합니다.
std::atomic is a C++ type that makes operations on one variable atomic, so concurrent reads and writes on that object are well-defined instead of a data race.
This article restates cppreference atomic and memory_order pages as of 2026-09-01; details can differ by standard version and implementation.
What does the atomic type guarantee?
One-line answer: Each instance or full specialization defines an atomic type, so a write by one thread and a read by another on the same object have defined behavior.
The C++11 template is provided by the <atomic> header. If one evaluation modifies a memory location while another reads or modifies it, and at least one evaluation is non-atomic, the program has a data race and undefined behavior when no happens-before relationship exists between them.
Atomic operations fall into three categories: store for writes, load for reads, and read-modify-write (RMW) for both reads and writes. The last category is one atomic operation rather than a sequence of separate operations. Depending on memory_order, accesses to atomic objects can also synchronize threads and order ordinary, non-atomic memory accesses.
The primary template requires a TriviallyCopyable type that can be copied and moved as required by the template, and a cv-qualified type cannot be used. Objects of this type are neither copyable nor movable. Pointer and integral specializations provide additional RMW capabilities such as fetch_add and fetch_sub; exchange and the two compare-exchange members are available as well.
C++20 also adds wait, notify_one, and notify_all members, but the main path here remains the C++11 load and store operations.
When should a mutex protect the operation instead?
One-line answer: The object makes one variable’s operation atomic, whereas a mutex protects a critical section that may contain several operations.
That distinction matters when choosing the protection boundary: one atomic read or write is different from keeping a multi-step update across several variables consistent. Making one object atomic does not turn an entire state transition into one atomic operation.
In memory-order terms, a mutex lock() operation is an acquire operation and unlock() is a release operation. Those relationships connect the ordering of memory accesses around the critical section.
is_lock_free() reports whether the particular atomic object is lock-free. Lock-free behavior is not guaranteed for every type, and an implementation may use an internal lock for some instantiations.
Standard volatile access is not atomic, so concurrent reads and writes constitute a data race.
How should a shared value be loaded and stored?
One-line answer: Use store for an atomic write and load for an atomic read; omitting the order argument selects memory_order_seq_cst for both.
Include the header, initialize the shared value, and make the read and write explicit with named members. This small C++11 example relies on their default ordering:
#include <atomic>
std::atomic<int> flag{0};
void publisher()
{
flag.store(1);
}
int observer()
{
return flag.load();
}
The write has the form void store(T desired, std::memory_order order = std::memory_order_seq_cst) noexcept, while the read has the form T load(std::memory_order order = std::memory_order_seq_cst) const noexcept. The first replaces the current value with desired atomically; the second atomically reads and returns the current value.
The write cannot take std::memory_order_consume, std::memory_order_acquire, or std::memory_order_acq_rel; using one of those orders gives undefined behavior. The read cannot take std::memory_order_release or std::memory_order_acq_rel. Keeping those member-specific restrictions visible prevents an invalid order choice.
The implicit conversion operator also reads the object, but a named load makes the intent clearer in introductory code. Integral specializations also expose RMW members such as fetch_add, along with exchange and the weak and strong compare-exchange operations; those are capabilities rather than the focus here.
For a separate contextual review of C++ interview concepts, see C++ 기술면접 질문 10가지; that article covers a different scope.
How should memory ordering be selected?
One-line answer: A memory-order argument specifies constraints on surrounding ordinary memory accesses in addition to making the atomic operation indivisible.
Library atomic operations default to memory_order_seq_cst. It adds a single total order in which threads observe modifications consistently, so it is the sensible starting point; choose a weaker order only when the required synchronization relationship is understood.
| Name | Short meaning |
|---|---|
memory_order_relaxed |
Guarantees only this operation’s atomicity, with no synchronization or ordering constraints on other reads and writes. |
memory_order_consume |
Represents a consume load; the documentation describes it as deprecated in C++26 and having the same effect as acquire. |
memory_order_acquire |
Applied to a load, it can make preceding writes from a release store on the same object visible when that store’s value is observed. |
memory_order_release |
Applied to a store, it makes preceding reads and writes in the current thread visible to another thread that acquires the same object. |
memory_order_acq_rel |
Applies both acquire and release semantics to a read-modify-write operation. |
memory_order_seq_cst |
Treats a load as acquire, a store as release, and an RMW as both, while also providing one total order for all modifications. |
For a counter that needs only atomic updates to its own value, memory_order_relaxed may be sufficient. It adds no synchronization or ordering for other memory accesses, so it is not by itself a publication mechanism for a payload.
For a flag that publishes a non-atomic payload, pair a release store with an acquire load. Once the acquire load observes the value written by the release store, the payload write sequenced before that store can be observed as well.
#include <atomic>
int payload = 0;
std::atomic<int> flag{0};
void publisher()
{
payload = 42;
flag.store(1, std::memory_order_release);
}
int observer()
{
while (flag.load(std::memory_order_acquire) == 0) {
}
return payload;
}
The important point is that release and acquire are paired through the same atomic object; the example is not merely relying on the flag’s indivisible update. A real program must also ensure that the synchronization path applies and that no conflicting payload write occurs.
FAQ
One-line answer: Start with named atomic reads and writes, then distinguish the boundary of a mutex-protected sequence from the ordering requirements of the data it publishes.
What should I use to read and write a shared C++ variable atomically? Use the C++11 atomic type for that variable’s operations. Decide separately whether a single object is enough or several operations need one protected critical section.
What is the default memory order for load and store operations? It is memory_order_seq_cst. Omitting the order argument on either member selects that default.
Can this object replace a mutex? It can suit an individual atomic operation on one variable, but a mutex remains the relevant boundary when several operations must form one critical section.
Is volatile atomic? No. Standard volatile access is not atomic, and concurrent reads and writes are a data race.
Where are the authoritative details?
One-line answer: The atomic type, ordering rules, and member preconditions are documented in the cited cppreference pages checked on 2026-09-01.
- std::atomic — overview of the atomic type,
store,load, RMW operations, constraints, andis_lock_free. - std::memory_order — data races, ordering constants, the sequentially consistent default, and acquire/release relationships.
- atomic::store — the store signature, default order, and disallowed order arguments.
- atomic::load — the load signature, default order, and disallowed order arguments.
'C++' 카테고리의 다른 글
| A Tour of C++ 1.9: 하드웨어 매핑과 포인터, 참조의 차이 (0) | 2026.09.18 |
|---|---|
| C++ 코딩 습관, Tour 1.10 Advice 정리 (0) | 2026.09.17 |
| C++ concepts, 템플릿 인자를 제약하는 법 (0) | 2026.09.15 |
| C++ 1.8 Tests 정리: 조건식, short-circuit, if 초기화, switch fall-through, vector::size() (0) | 2026.09.14 |
| C++ 1.7 포인터와 레퍼런스 정리: 주소, 배열, 참조, nullptr까지 (0) | 2026.09.13 |
