By | 2026년 8월 7일
Table of Contents

C/C++ 멀티스레드와 임베디드 커널 프로그래밍의 핵심: Atomic 연산 완벽 정리 (ATOMIC_INIT, atomic_read, atomic_set, atomic_cas)

멀티스레드(Multi-threaded) 환경이나 임베디드 RTOS / Linux 커널 프로그래밍을 진행할 때 가장 흔히 접하게 되는 문제 중 하나가 바로 경합 조건(Race Condition)입니다. 여러 스레드나 인터럽트 핸들러가 동시 다발적으로 같은 메모리 공간을 읽고 쓸 때 발생하며, 이는 예측할 수 없는 버그와 시스템 붕괴를 일으킵니다.

이러한 동기화 문제를 해결하기 위해 가장 흔히 떠올리는 방식은 Mutex(뮤텍스)나 Spinlock(스핀락)입니다. 하지만 간단한 상태 플래그 변경이나 카운팅 연산에 매번 Lock을 사용하는 것은 문맥 교환(Context Switch) 오버헤드성능 저하를 야기합니다.

이때 활용되는 것이 바로 CPU의 원자적 명령어를 활용한 Atomic(원자적) 연산입니다.

이번 포스팅에서는 임베디드 및 커널 개발에서 자주 사용되는 핵심 원자적 변수 및 연산자 4가지 (ATOMIC_INIT, atomic_read, atomic_set, atomic_cas)의 역할과 동작 원리, 그리고 실제 사용 예제까지 완벽히 정리해보겠습니다.


1. Atomic(원자적) 연산이란?

컴퓨터 과학에서 원자적(Atomic)이라는 말은 "더 이상 쪼갤 수 없는 단일 단위의 실행"을 의미합니다.

일반적으로 C언어에서 count++ 혹은 flag = 1 과 같은 문장은 assembly 레벨로 변환되면 다음과 같이 여러 단계로 나뉩니다:

  1. 메모리에서 값을 읽어 레지스터로 로드 (READ)
  2. 레지스터의 값을 변경 (MODIFY)
  3. 레지스터의 값을 다시 메모리에 저장 (WRITE)

만약 1번과 3번 단계 사이에서 다른 스레드가 개입하여 값을 읽거나 수정하면 데이터 오염이 발생합니다.

반면 Atomic 연산은 CPU 차원의 하드웨어 지원 명령어(x86의 LOCK Prefix, ARM의 LDREX/STREX 등)를 이용하여 [읽기-수정-쓰기]의 전 과정을 중간에 인터럽트나 다른 CPU 코어가 개입할 수 없는 하나의 단위로 처리합니다.


2. 핵심 Atomic 키워드 & 함수 해설

ATOMIC_INIT(val) — 원자적 변수의 선언 및 초기화

ATOMIC_INITatomic_t 타입 변수를 정적으로 초기화할 때 사용되는 매크로입니다.

  • 기능: atomic 변수를 선언함과 동시에 초기 값을 안전하게 설정합니다.
  • 주요 용도: 전역 변수나 static 변수를 선언할 때 컴파일 타임에 초기화하는 용도로 사용됩니다.
// 예시: DHCP IP 할당 플래그를 0(Unbound)으로 정적 초기화
static atomic_t ethernet_dhcp_bound_flag = ATOMIC_INIT(0);

atomic_read(const atomic_t *v) — 안전한 원자적 값 읽기

  • 기능: atomic 변수 v의 현재 값을 안전하게 읽어옵니다.
  • 왜 일반 읽기 대신 사용할까?: 컴파일러 최적화로 인해 변수가 레지스터에 캐싱되거나, 명령어가 재배치(Reordering)되는 현상을 방지합니다. 또한 메모리 배리어(Memory Barrier) 효과를 동반하여 항상 메모리의 최신 상태를 읽어올 수 있도록 보장합니다.
// 예시: DHCP 할당 상태 읽기
int is_bound = atomic_read(&ethernet_dhcp_bound_flag);

if (is_bound) {
    // 네트워크 통신 시작
}

atomic_set(atomic_t *v, int i) — 안전한 원자적 값 쓰기

  • 기능: atomic 변수 v에 정수 값 i를 원자적으로 대입(쓰기)합니다.
  • 특징: 단순한 대입 연산이지만 타 스레드와의 읽기/쓰기 충돌 없이 깔끔하게 값을 변경합니다.
// 예시: DHCP 할당 성공 시 플래그를 1로 변경
void on_dhcp_success(void) {
    // ... IP 관련 설정 완료 후 ...
    atomic_set(&ethernet_dhcp_bound_flag, 1);
}

atomic_cas(atomic_t *v, int old_val, int new_val) — Compare-And-Swap (비교 및 교환)

atomic_cas (또는 Linux 커널의 atomic_cmpxchg)는 Lock-Free 알고리즘의 가장 핵심이 되는 연산입니다.

  • 기능:
    1. 변수 v의 현재 값이 old_val과 일치하는지 비교합니다.
    2. 일치한다면: v의 값을 new_val로 교환하고 성공(True 또는 이전 값)을 반환합니다.
    3. 일치하지 않는다면: 다른 스레드가 값을 중간에 바꾼 것으로 판단하여 교환하지 않고 실패(False)를 반환합니다.
  • 핵심 포인트: 비교와 교환이라는 두 가지 작업이 단 하나의 원자적 명령으로 실행됩니다.

atomic_cas 동작 개념 (C언어 슈도코드)

// 실제로는 이 전체 로직이 CPU 내부에서 단 하나의 원자적 명령어로 실행됨!
bool atomic_cas(atomic_t *ptr, int expected_val, int new_val) {
    if (*ptr == expected_val) {
        *ptr = new_val;
        return true;  // 성공
    }
    return false;     // 실패 (다른 스레드가 이미 값을 변경함)
}

3. 실전 응용: Lock-Free 패턴과 Retry Loop

atomic_cas는 단독으로 사용하기보다 성공할 때까지 반복하는 Retry Loop 구조로 많이 활용됩니다.

예를 들어, 여러 스레드가 공유 카운터를 안전하게 증가시키려고 할 때 Lock(Mutex) 없이 다음과 같이 구현할 수 있습니다.

#include <stdio.h>
#include <stdbool.h>

// atomic 변수 선언 및 초기화
static atomic_t g_counter = ATOMIC_INIT(0);

// Lock-Free 카운터 증가 함수
void safe_increment(void) {
    int current_val;

    do {
        // 1. 현재 카운트 값을 읽어옴
        current_val = atomic_read(&g_counter);

        // 2. 내가 읽은 current_val이 유지되고 있다면 current_val + 1로 업데이트 시도
        // 중간에 다른 스레드가 g_counter를 바꿔버렸다면 atomic_cas는 false를 반환하고 루프 재시도
    } while (!atomic_cas(&g_counter, current_val, current_val + 1));
}

4. 요약 및 정리

함수/매크로 주요 기능 주요 사용처
ATOMIC_INIT(val) atomic_t 변수를 정적으로 초기화 전역/static atomic 변수 선언 시
atomic_read(v) 메모리에서 변수 값을 원자적으로 읽음 상태 체크 및 조건문 검사
atomic_set(v, i) 변수에 새로운 정수 값을 원자적으로 저장 상태 단순 변경 및 상태 플래그 설정
atomic_cas(v, old, new) 변수 값이 old와 같으면 new로 교환 Lock-Free 알고리즘, Spinlock 구현

마무리

Mutex나 Semaphore는 안전하지만 스레드가 대기 상태(Blocked)로 전환되는 비용이 발생합니다. 반면, 간단한 상태 플래그나 카운터 관리 시 Atomic 연산 (ATOMIC_INIT, atomic_read, atomic_set, atomic_cas)을 활용하면 동기화 오버헤드를 최소화하면서도 완벽한 Thread-Safety를 확보할 수 있습니다.

임베디드 드라이버 개발이나 고성능 멀티스레드 애플리케이션을 설계할 때 Atomic 연산의 매커니즘을 꼭 숙지하고 적절히 활용해보세요!

답글 남기기