원자 비교·교환: 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 연산의 메모리 모델은 각각 succfail이에요. (1)(2) 버전은 기본으로 memory_order_seq_cst를 사용해요.

여기서 한 가지 재미있는 점은 weak 형태((2), (4))예요. 그들은 설사 *obj == *expected이더라도 마치 다르게 보이는 것처럼 가짜로(spuriously) 실패할 수 있어요. 그래서 비교·교환을 루프 안에서 쓸 때는 weak 버전이 일부 플랫폼에서 더 좋은 성능을 보여 줍니다. 반대로 weak 버전이라면 루프가 필요하지만 strong 버전이라면 루프가 필요 없을 때는 strong 쪽이 더 낫고요. 요점은, 반복 시도할 거라면 weak, 아니면 strong이라는 거예요.

이 함수는 모든 원자 객체 타입 A에 대해 정의된 제네릭 함수예요. 인자는 volatile 원자 타입의 포인터라서 비휘발성·휘발성(예: 메모리 매핑 I/O) 원자 객체의 주소를 모두 받아들여요. CA에 대응하는 비원자 타입이에요.

제네릭 함수의 이름이 매크로인지 외부 연결을 가진 식별자인지는 규정되지 않았어요. 매크로 정의를 막고 실제 함수에 접근하려고 (atomic_compare_exchange)(...)처럼 괄호로 감싸 쓰는 경우, 또는 프로그램이 제네릭 함수 이름과 같은 외부 식별자를 정의하는 경우의 동작은 미정의(undefined)예요.

매개변수

매개변수 설명
obj 검사하고 수정할 원자 객체를 가리키는 포인터
expected 원자 객체에서 찾을 것으로 기대하는 값을 가리키는 포인터
desired 값이 기대와 같을 때 원자 객체에 저장할 값
succ 비교가 성공한 read-modify-write 연산의 메모리 동기화 순서. 모든 값 허용
fail 비교가 실패한 load 연산의 메모리 동기화 순서. memory_order_releasememory_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: 플래그 기반의 비교·교환류 동작.