atomic_fetch_sub, atomic_fetch_sub_explicit

atomic_fetch_sub, atomic_fetch_sub_explicit

원자 객체의 값을 하나 빼면서, 빼기 전의 값을 한 번에 얻고 싶을 때가 있어요. 별도로 읽고 쓰면 그 사이에 다른 스레드가 끼어들 수 있으니까요. 그때 쓰는 함수가 atomic_fetch_sub예요. <stdatomic.h>에 정의돼 있어요.

출처: cppreference

본문

atomic_fetch_sub는 원자 객체가 가리키는 값에서 arg를 뺀 결과로 그 값을 원자적으로 바꾸고, 바꾸기 이전에 담고 있던 값을 돌려줘요. 이때 수행되는 연산은 read-modify-write, 즉 읽고-고치고-다시 쓰는 원자 연산이에요.

  • 첫 번째 버전은 메모리 접근 순서를 memory_order_seq_cst(순차 일관성)로 정렬해요.
  • 두 번째 버전(_explicit)은 인자로 받은 order에 따라 메모리 접근 순서를 정렬해요.

시그니처

<stdatomic.h>에 정의돼 있어요.

C atomic_fetch_sub( volatile A* obj, M arg );                  // (1) since C11
C atomic_fetch_sub_explicit( volatile A* obj, M arg,
                             memory_order order );             // (2) since C11

이 함수는 모든 원자 객체 타입 A에 대해 동작하는 제네릭 함수예요. 인자는 volatile 원자 타입에 대한 포인터라서, 일반 객체든 메모리 맵 I/O처럼 volatile 처리된 객체든 주소를 넘길 수 있어요.

  • MA가 원자 정수 타입이면 그에 대응하는 비원자 타입이고, A가 원자 포인터 타입이면 ptrdiff_t예요.

매개변수

매개변수 설명
obj 수정할 원자 객체를 가리키는 포인터
arg 원자 객체에 저장된 값에서 뺄 값
order 이 연산의 메모리 동기화 순서. 모든 값이 허용돼요

반환값

obj가 가리키는 원자 객체가 이전에 담고 있던 값을 돌려줘요.

세부 동작

  • 부호 있는 정수 타입은 2의 보수(two's complement) 표현으로 산술이 정의돼요. 따라서 정의되지 않은 결과가 없어요.
  • 포인터 타입은 결과가 정의되지 않은 주소가 될 수 있지만, 그 외에는 정의되지 않은 동작이 없어요.
  • 제네릭 함수의 이름이 매크로인지 외부 링크를 가진 식별자인지는 정해져 있지 않아요. 매크로 정의를 억제해 실제 함수에 접근하거나((atomic_fetch_sub)(...)처럼 괄호로 감싸기), 프로그램이 같은 이름의 외부 식별자를 정의하면 동작은 정의되지 않아요.

예시

원자 카운터에서 값을 하나씩 빼면서, 그 전 값까지 확인하는 흐름이에요.

#include <stdatomic.h>
#include <stdio.h>

int main(void)
{
    atomic_int count = ATOMIC_VAR_INIT(10);
    int prev = atomic_fetch_sub(&count, 3);
    printf("before=%d after=%d\n", prev, atomic_load(&count));
}

여기서 prev는 빼기 전 값인 10, 그리고 빼고 난 뒤의 count는 7이 돼요.

더 알아보기

  • 더하는 버전은 atomic_fetch_add 문서에서 다뤄요.
  • 메모리 순서의 개념은 memory_order 문서를 참고해요.
  • cppreference의 atomic_fetch_sub 원문을 참고해요.