원자 덧셈: atomic_fetch_add(C11)
원자 덧셈: atomic_fetch_add(C11)
원자 카운터에 값을 더할 때는 '읽고, 더하고, 다시 써 넣는' 동작이 원자적으로 이뤄져야 해요. 그런데 대부분 값을 더하기만 하면 되지, 이전 값은 굳이 필요 없잖아요? 그래도 함수는 이전 값을 돌려주도록 설계돼 있어요. 그 함수가 **atomic_fetch_add**예요. 이름의 fetch가 '이전 값을 가져온다'는 뜻이죠.
출처: cppreference
본문
시그니처
<stdatomic.h> 헤더에 정의돼 있어요.
| 형태 | 주석 |
|---|---|
C atomic_fetch_add(volatile A* obj, M arg); |
(1) C11부터 |
C atomic_fetch_add_explicit(volatile A* obj, M arg, memory_order order); |
(2) C11부터 |
동작 방식
obj가 가리키는 값과 arg를 더한 결과로 그 값을 원자적으로 교체해요. 그리고 obj가 이전에 담고 있던 값을 반환해요. read-modify-write 연산이에요. (1) 버전은 memory_order_seq_cst로, (2) 버전은 order로 메모리 접근을 정렬해요.
이 함수는 모든 원자 객체 타입 A에 대해 정의된 제네릭 함수예요. 인자는 volatile 원자 타입의 포인터라서 비휘발성·휘발성(예: 메모리 매핑 I/O) 원자 객체의 주소를 모두 받아들여요. M은 A가 원자 정수 타입이면 A에 대응하는 비원자 타입이고, A가 원자 포인터 타입이면 ptrdiff_t예요.
제네릭 함수의 이름이 매크로인지 외부 연결을 가진 식별자인지는 규정되지 않았어요. 매크로 정의를 막고 실제 함수에 접근하려고 (atomic_fetch_add)(...)처럼 괄호로 감싸 쓰는 경우, 또는 프로그램이 제네릭 함수 이름과 같은 외부 식별자를 정의하는 경우의 동작은 미정의(undefined)예요.
부호 있는 정수 타입에 대해서는 산술이 2의 보수 표현을 쓰는 것으로 정의돼요. 즉 미정의 결과가 없습니다. 포인터 타입에 대해서는 결과가 미정의 주소일 수 있지만, 연산 자체에는 미정의 동작이 없어요.
매개변수
| 매개변수 | 설명 |
|---|---|
obj |
수정할 원자 객체를 가리키는 포인터 |
arg |
원자 객체에 저장된 값에 더할 값 |
order |
이 연산의 메모리 동기화 순서. 모든 값 허용 |
반환값
obj가 이전에 담고 있던 값을 반환해요.
예제
열 개의 스레드가 원자 정수 카운터를 각각 천 번씩 증가시키는 코드예요. atomic_fetch_add_explicit로 원자적으로 더하면 정확히 10000이 되지만, 그냥 ++cnt는 데이터 경쟁 때문에 일부 갱신이 누락돼요.
#include <stdio.h>
#include <threads.h>
#include <stdatomic.h>
atomic_int acnt;
int cnt;
int f(void* thr_data)
{
for(int n = 0; n < 1000; ++n) {
atomic_fetch_add_explicit(&acnt, 1, memory_order_relaxed); // atomic
++cnt; // undefined behavior, in practice some updates missed
}
return 0;
}
int main(void)
{
thrd_t thr[10];
for(int n = 0; n < 10; ++n)
thrd_create(&thr[n], f, NULL);
for(int n = 0; n < 10; ++n)
thrd_join(thr[n], NULL);
printf("The atomic counter is %u\n", acnt);
printf("The non-atomic counter is %u\n", cnt);
}
가능한 출력:
The atomic counter is 10000
The non-atomic counter is 9511
같은 조건에서 일반 카운터 cnt는 실행할 때마다 값이 들쭉날쭉하지만, 원자 카운터 acnt는 항상 10000이 돼요. 이 예제에서는 초기화 직후라 메모리 순서가 느슨해도(memory_order_relaxed) 결과에는 문제가 없어요.
더 알아보기
atomic_fetch_sub: 원자 뺄셈.memory_order: 메모리 동기화 순서.atomic_exchange,atomic_compare_exchange: 다른 원자 교환 연산.