원자 비교·교환: atomic_compare_exchange(C11)
원자 비교·교환: atomic_compare_exchange(C11)
원자 객체의 값을 바꾸려는데, 다른 스레드가 이미 값을 고쳤다면 곤란하겠죠. 그래서 '이 값이 여전히 내가 기대하는 값이면 교환하고, 아니면 그대로 두는' 연산이 필요해요. 이걸 한 번에 원자적으로 수행하는 함수가 atomic_compare_exchange 계열이에요. 더 이상 사용하지 않고 재사용하는 동시성 자료구조에서 빠질 수 없는 기본기입니다.
출처: cppreference
본문
시그니처
<stdatomic.h> 헤더에 정의돼 있어요.
| 형태 | 주석 |
|---|---|
_Bool atomic_compare_exchange_strong(volatile A* obj, C* expected, C desired); |
(1) C11부터 |
_Bool atomic_compare_exchange_weak(volatile A* obj, C* expected, C desired); |
(2) C11부터 |
_Bool atomic_compare_exchange_strong_explicit(volatile A* obj, C* expected, C desired, memory_order succ, memory_order fail); |
(3) C11부터 |
_Bool atomic_compare_exchange_weak_explicit(volatile A* obj, C* expected, C desired, memory_order succ, memory_order fail); |
(4) C11부터 |
동작 방식
obj가 가리키는 메모리의 내용과 expected가 가리키는 메모리의 내용을 원자적으로 비교해요. 만약 비트 단위로 같다면 전자를 desired로 바꿔요(read-modify-write 연산). 같지 않다면 obj가 가리키는 메모리의 실제 내용을 *expected로 읽어 와요(load 연산).
read-modify-write와 load 연산의 메모리 모델은 각각 succ와 fail이에요. (1)(2) 버전은 기본으로 memory_order_seq_cst를 사용해요.
여기서 한 가지 재미있는 점은 weak 형태((2), (4))예요. 그들은 설사 *obj == *expected이더라도 마치 다르게 보이는 것처럼 가짜로(spuriously) 실패할 수 있어요. 그래서 비교·교환을 루프 안에서 쓸 때는 weak 버전이 일부 플랫폼에서 더 좋은 성능을 보여 줍니다. 반대로 weak 버전이라면 루프가 필요하지만 strong 버전이라면 루프가 필요 없을 때는 strong 쪽이 더 낫고요. 요점은, 반복 시도할 거라면 weak, 아니면 strong이라는 거예요.
이 함수는 모든 원자 객체 타입 A에 대해 정의된 제네릭 함수예요. 인자는 volatile 원자 타입의 포인터라서 비휘발성·휘발성(예: 메모리 매핑 I/O) 원자 객체의 주소를 모두 받아들여요. C는 A에 대응하는 비원자 타입이에요.
제네릭 함수의 이름이 매크로인지 외부 연결을 가진 식별자인지는 규정되지 않았어요. 매크로 정의를 막고 실제 함수에 접근하려고 (atomic_compare_exchange)(...)처럼 괄호로 감싸 쓰는 경우, 또는 프로그램이 제네릭 함수 이름과 같은 외부 식별자를 정의하는 경우의 동작은 미정의(undefined)예요.
매개변수
| 매개변수 | 설명 |
|---|---|
obj |
검사하고 수정할 원자 객체를 가리키는 포인터 |
expected |
원자 객체에서 찾을 것으로 기대하는 값을 가리키는 포인터 |
desired |
값이 기대와 같을 때 원자 객체에 저장할 값 |
succ |
비교가 성공한 read-modify-write 연산의 메모리 동기화 순서. 모든 값 허용 |
fail |
비교가 실패한 load 연산의 메모리 동기화 순서. memory_order_release나 memory_order_acq_rel은 쓸 수 없고, succ보다 강한 순서를 지정할 수 없음 |
반환값
비교 결과를 반환해요. *obj == *exp였으면 true, 아니면 false.
주의할 점
atomic_compare_exchange_* 계열의 동작은 다음 코드가 원자적으로 실행된 것과 같아요.
if (memcmp(obj, expected, sizeof *obj) == 0) {
memcpy(obj, &desired, sizeof *obj);
return true;
} else {
memcpy(expected, obj, sizeof *obj);
return false;
}
즉 성공하면 *expected는 그대로 두고, 실패하면 *expected에 실제 값을 채워 넣어서 호출자가 다시 시도할 때 쓸 수 있게 해 줍니다.
더 알아보기
atomic_exchange: 조건 없이 값을 맞바꾸는 원자 연산.memory_order: 메모리 동기화 순서.atomic_flag_test_and_set: 플래그 기반의 비교·교환류 동작.