C++ std::atomic, 데이터 레이스를 막는 법

std::atomic은 한 변수에 대한 연산을 원자적으로 만들어 데이터 레이스를 피하게 하는 C++ 타입입니다.

이 글은 cppreference의 atomic·memory_order 문서를 2026-09-01 기준으로 정리한 일반 설명이며, 표준 버전과 구현에 따라 세부 동작은 달라질 수 있습니다.

std::atomic은 무엇인가?

한 줄 답: 이 타입의 각 인스턴스와 완전 특수화는 원자 타입을 정의하며, 같은 객체를 한 스레드가 쓰고 다른 스레드가 읽어도 해당 접근의 동작은 정의됩니다.

<atomic> 헤더의 이 C++11 템플릿은 한 메모리 위치에 대한 접근을 원자 연산으로 표현합니다. 한 평가가 메모리 위치를 수정하고 다른 평가가 같은 위치를 읽거나 수정할 때 둘 중 하나 이상이 비원자 연산이면, 두 평가 사이에 happens-before 관계가 없는 경우 데이터 레이스가 되어 프로그램의 동작이 정의되지 않습니다. 원자 객체에 대한 읽기와 쓰기는 이 조건에서 해당 객체의 연산을 원자적으로 처리합니다.

원자 연산은 쓰기인 store, 읽기인 load, 읽은 뒤 쓰는 read-modify-write로 나뉩니다. read-modify-write는 읽기와 쓰기를 여러 개의 분리된 연산으로 처리하는 것이 아니라 하나의 원자 연산으로 처리합니다. 원자 객체에 대한 접근은 스레드 간 동기화를 만들 수 있으며, memory_order에 따라 비원자 메모리 접근의 순서도 정할 수 있습니다.

기본 템플릿은 TriviallyCopyable이면서 복사 생성·대입과 이동 생성·대입 조건을 만족하는 타입에 사용할 수 있으며, cv 한정 타입은 대상이 될 수 없습니다. 반대로 이 타입의 객체 자체는 복사할 수도 이동할 수도 없습니다. 포인터와 정수 특수화에는 fetch_addfetch_sub 같은 추가 RMW 연산이 있고, exchange, compare_exchange_weak, compare_exchange_strong도 같은 객체에서 사용하는 RMW 멤버입니다.

뮤텍스와 무엇이 다른가?

한 줄 답: 이 객체는 변수 하나의 연산을 원자적으로 다루고, 뮤텍스는 여러 연산을 포함할 수 있는 임계 구역을 잠그는 도구입니다.

따라서 한 변수의 값을 한 번 읽거나 쓰는 연산을 보호하는 문제와, 여러 연산을 하나의 일관된 임계 구역으로 묶는 문제는 구분해야 합니다. 원자 타입을 사용한다고 해서 여러 변수에 걸친 여러 단계의 상태 변경 전체가 하나의 원자 연산이 되는 것은 아닙니다.

메모리 순서 관점에서 뮤텍스의 lock()은 acquire 연산이고 unlock()은 release 연산이므로, 잠금 도구도 획득과 해제를 기준으로 메모리 접근의 순서를 연결합니다.

is_lock_free()는 해당 원자 객체가 lock-free인지 확인하는 함수입니다. 모든 타입이 lock-free라는 보장은 없으며, 구현에 따라 이 객체의 연산이 내부 잠금을 사용할 수도 있습니다.

표준 volatile 접근은 원자적이지 않으며 동시 읽기·쓰기는 데이터 레이스입니다.

load와 store는 어떻게 쓰나?

한 줄 답: store는 값을 원자적으로 바꾸고 load는 현재 값을 원자적으로 읽으며, 둘 다 인자를 생략하면 memory_order_seq_cst를 사용합니다.

먼저 <atomic>을 포함하고 초기값을 지정한 뒤, 이름 있는 storeload로 쓰기와 읽기를 표현합니다. 다음 예시처럼 첫 번째 인자를 생략하면 두 연산 모두 기본 메모리 순서로 동작합니다.

#include <atomic>

std::atomic<int> flag{0};

void publisher()
{
    flag.store(1);
}

int observer()
{
    return flag.load();
}

void store(T desired, std::memory_order order = std::memory_order_seq_cst) noexcept는 현재 값을 desired로 원자적으로 바꾸고, T load(std::memory_order order = std::memory_order_seq_cst) const noexcept는 현재 값을 원자적으로 읽어 반환합니다. 인자를 생략한 두 연산의 기본값은 std::memory_order_seq_cst입니다. 암시적 변환 연산자도 값을 읽지만, 동작을 설명하거나 의도를 드러낼 때는 이름 있는 load를 우선 사용하는 편이 명확합니다.

storestd::memory_order_consume, std::memory_order_acquire, std::memory_order_acq_rel을 전달하면 동작이 정의되지 않습니다. loadstd::memory_order_release 또는 std::memory_order_acq_rel을 전달해도 동작이 정의되지 않으므로, 쓰기와 읽기에 허용되는 메모리 순서를 구분해야 합니다. 정수 특수화의 fetch_add 등과 exchange, compare_exchange_weak, compare_exchange_strong은 같은 객체에서 사용하는 RMW 멤버라는 점만 기억하면 됩니다.

memory_order는 왜 고르나?

한 줄 답: memory_order는 원자성 외에 원자 연산 주위의 일반 메모리 접근까지 어떤 순서 제약을 둘지 지정합니다.

라이브러리의 원자 연산은 기본적으로 memory_order_seq_cst를 사용하며, 이 순서는 모든 스레드가 원자 객체의 수정 사항을 같은 순서로 관측하는 단일 전체 순서를 추가합니다. 처음에는 기본값으로 시작하고, 원자성 외에 필요한 동기화 관계가 분명할 때만 더 약한 순서를 추가 인자로 고르는 흐름이 적절합니다.

이름짧은 의미
memory_order_relaxed다른 읽기·쓰기에 동기화·순서 제약은 두지 않고 해당 연산의 원자성만 보장합니다.
memory_order_consumeconsume load이며, C++26에서는 deprecated되고 acquire와 같은 효과를 내는 순서로 소개됩니다.
memory_order_acquireload의 acquire이며, 같은 원자 객체에 release한 다른 스레드의 쓰기를 현재 스레드에서 볼 수 있게 합니다.
memory_order_releasestore의 release이며, 현재 스레드의 앞선 쓰기를 같은 원자 객체를 acquire한 다른 스레드에 보이게 합니다.
memory_order_acq_relRMW 연산에 acquire와 release를 함께 적용합니다.
memory_order_seq_cstload는 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

한 줄 답: 공유 변수의 원자적 읽기·쓰기는 이 타입의 loadstore에서 시작하고, 여러 단계의 임계 구역과 메모리 순서 요구는 별도로 구분합니다.

C++에서 공유 변수를 원자적으로 읽고 쓰려면 무엇을 사용합니까? C++11의 이 타입으로 해당 변수의 연산을 원자적으로 만듭니다. 한 객체의 읽기와 쓰기를 원자적으로 처리해야 하는지, 여러 연산을 함께 보호해야 하는지는 별도로 판단합니다.

load와 store의 기본 memory_order는 무엇입니까? memory_order_seq_cst입니다. 두 연산에서 순서 인자를 생략하면 이 기본값이 사용됩니다.

뮤텍스 대신 이 객체를 사용하면 됩니까? 한 변수에 대한 개별 원자 연산이면 이 객체를 검토할 수 있지만, 여러 연산을 하나의 임계 구역으로 묶어야 한다면 뮤텍스와 보호 범위를 비교해야 합니다.

출처

한 줄 답: 본문은 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::storestore 시그니처, 기본 순서와 허용하지 않는 순서 인자를 확인했습니다.
  • atomic::loadload 시그니처, 기본 순서와 허용하지 않는 순서 인자를 확인했습니다.