프로그래밍/C,C++

[Modern C++] std::atomic

GONII 2025. 3. 21. 14:22

🔹 std::atomic 개념과 활용

std::atomic은 C++에서 멀티스레드 환경에서의 동기화 없이 원자적 연산을 수행할 수 있도록 제공되는 템플릿 클래스입니다.
이를 통해 Lock-Free 또는 Wait-Free 알고리즘을 구현할 수 있으며, CPU의 원자적 연산을 직접 활용하여 성능을 높일 수 있습니다.


1. std::atomic의 특징

멀티스레드 안전(Thread-safe)
Lock-Free 지원 (하드웨어 CAS 등 사용)
메모리 순서 제어 가능 (memory_order 옵션)
기본 데이터 타입, 사용자 정의 타입에도 적용 가능


2. std::atomic 기본 사용법

#include <iostream>
#include <atomic>
#include <thread>

std::atomic<int> counter(0);

void increment() {
    for (int i = 0; i < 10000; ++i) {
        counter.fetch_add(1, std::memory_order_relaxed); // 원자적 증가
    }
}

int main() {
    std::thread t1(increment);
    std::thread t2(increment);

    t1.join();
    t2.join();

    std::cout << "Final Counter: " << counter.load() << std::endl;
}

설명:

  • std::atomic<int>을 사용하여 동기화 없이 원자적으로 증가.
  • fetch_add(1, std::memory_order_relaxed)를 사용해 락 없이 counter를 증가.
  • 두 개의 스레드가 동시에 증가 작업을 수행해도 데이터 경쟁(Race Condition) 없이 안전하게 동작.

3. 주요 멤버 함수

함수 설명
load() 원자적으로 변수 값을 읽음
store(value) 원자적으로 변수 값을 설정
fetch_add(n) += n 연산을 원자적으로 수행
fetch_sub(n) -= n 연산을 원자적으로 수행
fetch_and(n) &= n 연산을 원자적으로 수행
fetch_or(n) |= n  연산을 원자적으로 수행
fetch_xor(n) ^= n 연산을 원자적으로 수행
compare_exchange_weak(expected, desired) CAS(Compare-And-Swap) 연산 수행
compare_exchange_strong(expected, desired) CAS 연산 수행 (더 강력한 보장)

4. compare_exchange_weak (CAS 연산)

🔹 **CAS(Compare-And-Swap)**은 Lock-Free 알고리즘에서 핵심적으로 사용됩니다.
🔹 compare_exchange_weak()를 활용하여 값이 예상한 값(expected)일 때만 새로운 값(desired)으로 변경할 수 있습니다.
🔹 실패할 경우 expected가 현재 값으로 업데이트되므로 다시 시도 가능.

#include <iostream>
#include <atomic>
#include <thread>

std::atomic<int> value(10);

void cas_example() {
    int expected = 10;
    int desired = 20;

    if (value.compare_exchange_weak(expected, desired)) {
        std::cout << "CAS 성공! value = " << value.load() << std::endl;
    } else {
        std::cout << "CAS 실패! 현재 value = " << value.load() << std::endl;
    }
}

int main() {
    std::thread t1(cas_example);
    std::thread t2(cas_example);

    t1.join();
    t2.join();

    return 0;
}

설명

  • compare_exchange_weak(expected, desired)는 expected 값과 현재 값이 같을 경우만 값을 변경.
  • 스레드 간 충돌이 발생하면 CAS가 실패할 수 있으며, 이를 기반으로 Lock-Free 알고리즘을 구성할 수 있음.

5. 메모리 순서 (memory_order)

🔹 std::atomic 연산은 기본적으로 메모리 순서(memory_order) 를 지정할 수 있음.
🔹 기본값은 memory_order_seq_cst (Sequentially Consistent Order) 이지만, 성능 최적화를 위해 더 약한 순서를 사용할 수 있음.

메모리 순서 설명 성능
memory_order_seq_cst 가장 강력한 순서 보장 (기본값) 느림
memory_order_acquire 읽기 전에 이전 연산들이 완료되도록 보장 중간
memory_order_release 쓰기 이후 연산들이 순서대로 실행되도록 보장 중간
memory_order_acq_rel acquire + release 조합 중간
memory_order_relaxed 메모리 순서 보장 없음 (최대 성능) 빠름

🔹 예제 (memory_order_relaxed 사용)

std::atomic<int> value[5] = { 0, };

void thread_func()
{
    value[0].store(1, std::memory_order_relaxed);
    value[1].store(2, std::memory_order_relaxed);
    value[2].store(3, std::memory_order_relaxed);
    value[3].store(4, std::memory_order_relaxed);
    value[4].store(5, std::memory_order_relaxed);
    // std::memory_order_relaxed로 사용된 경우 어떤 변수가 먼저 수행될지 보장 되지 않는다.
    // 0보다 1번이 먼저 연산될 수도 있음
    // 컴파일러나 CPU의 최적화에 따라 value[0]이 value[1]보다 먼저 저장될 수도 있고,
    // 반대일 수도 있음
}

✅ memory_order_relaxed를 사용하면 순서는 보장하지 않지만 원자적 연산은 보장됨.
✅ 따라서 일부 Lock-Free 알고리즘에서 성능 최적화를 위해 활용 가능.


6. std::atomic을 활용한 Lock-Free 스택 구현

#include <iostream>
#include <atomic>

struct Node {
    int data;
    Node* next;
};

class LockFreeStack {
    std::atomic<Node*> head;

public:
    void push(int value) {
        Node* new_node = new Node{value, nullptr};
        while (true) {
            Node* old_head = head.load();
            new_node->next = old_head;
            if (head.compare_exchange_weak(old_head, new_node)) {
                return;
            }
        }
    }

    bool pop(int& value) {
        while (true) {
            Node* old_head = head.load();
            if (!old_head) return false;
            if (head.compare_exchange_weak(old_head, old_head->next)) {
                value = old_head->data;
                delete old_head;
                return true;
            }
        }
    }
};

설명:

  • compare_exchange_weak을 사용하여 Lock-Free 방식으로 스택을 구현.
  • while (true) 루프에서 원자적 연산을 사용해 충돌이 발생해도 재시도할 수 있도록 처리.

7. std::atomic과 FAA(Fetch-And-Add)

🔹 fetch_add() 연산은 FAA(Fetch-And-Add)라고도 하며, 원자적으로 값을 증가시키는 연산입니다.
🔹 일부 CPU에서는 FAA가 CAS보다 성능이 뛰어나므로 Lock-Free 카운터 등에 사용됩니다.

std::atomic<int> counter(0);

void increment() {
    counter.fetch_add(1, std::memory_order_relaxed);
}

fetch_add는 락 없이 안전하게 증가 연산을 수행.


8. 정리

✔ std::atomic을 사용하면 락 없이 동기화가 가능한 원자적 연산 수행 가능
✔ compare_exchange_weak (CAS)을 이용하여 Lock-Free 알고리즘 구현 가능
✔ fetch_add()는 FAA(Fetch-And-Add) 연산을 통해 락 없이 카운터 증가 가능
✔ memory_order를 조절하여 성능 최적화 가능 (memory_order_relaxed 등)

📌 즉, std::atomic은 Lock-Free & Wait-Free 알고리즘을 구현할 때 핵심적인 역할을 함! 🚀


reference
https://narakit.tistory.com/194

 

[C++ Thread] Lock-Free Programming - (1) Lock Free ? / Lock Free Stack

Lock-Free 코드란? - Concurrency와 Scalibility를 가지는 동기화 방법 코드상 존재하는 블로킹/기다림을 제거하거나 줄이는 것 - 블록킹을 사용하면서 생기는 잠재적인 문제 경쟁 조건 (Race Condition): 잘못

narakit.tistory.com

https://medium.com/@hoangxuantoank13/introduction-to-lock-free-and-how-it-is-used-to-implement-thread-safe-non-blocking-queue-in-java-b0759a25058a

 

Introduction to Lock-Free and How it is used to implement Thread-safe Non-Blocking Queue in Java

Introduction

medium.com

https://modoocode.com/271

 

씹어먹는 C++ - <15 - 3. C++ memory order 와 atomic 객체>

여러분의 코드는 여러분이 생각하는 순서로 작동하지 않습니다. (단일 쓰레드 관점에서) 결과값이 동일하다면 컴파일러와 CPU 는 명령어의 순서를 재배치 할 수 있습니다. 문제는 이렇게 마음대

modoocode.com

https://en.cppreference.com/w/cpp/thread

 

Concurrency support library (since C++11) - cppreference.com

Concurrency support library C++ includes built-in support for threads, atomic operations, mutual exclusion, condition variables, and futures. [edit] Threads Threads enable programs to execute across several processor cores. manages a separate thread (class

en.cppreference.com

 

반응형