Namespaces
Variants

std:: memory_order

From cppreference.net
Concurrency support library
Threads
(C++11)
(C++20)
this_thread namespace
(C++11)
(C++11)
Cooperative cancellation
Mutual exclusion
Generic lock management
Condition variables
(C++11)
Semaphores
Latches and Barriers
(C++20)
(C++20)
Futures
(C++11)
(C++11)
(C++11)
Safe reclamation
Hazard pointers
Atomic types
(C++11)
(C++20)
Initialization of atomic types
(C++11) (deprecated in C++20)
(C++11) (deprecated in C++20)
Memory ordering
memory_order
(C++11)
(C++11) (deprecated in C++26)
Free functions for atomic operations
Free functions for atomic flags
헤더 파일에 정의됨 <atomic>
enum memory_order

{
memory_order_relaxed,
memory_order_consume,
memory_order_acquire,
memory_order_release,
memory_order_acq_rel,
memory_order_seq_cst

} ;
(C++11부터)
(C++20까지)
enum class memory_order : /* unspecified */

{
relaxed, consume, acquire, release, acq_rel, seq_cst
} ;
inline constexpr memory_order memory_order_relaxed = memory_order :: relaxed ;
inline constexpr memory_order memory_order_consume = memory_order :: consume ;
inline constexpr memory_order memory_order_acquire = memory_order :: acquire ;
inline constexpr memory_order memory_order_release = memory_order :: release ;
inline constexpr memory_order memory_order_acq_rel = memory_order :: acq_rel ;

inline constexpr memory_order memory_order_seq_cst = memory_order :: seq_cst ;
(C++20부터)

std::memory_order 는 원자적 연산을 중심으로 일반적인 비원자적 메모리 접근을 포함한 메모리 접근들이 어떻게 순서화되어야 하는지를 지정합니다. 다중 코어 시스템에서 아무런 제약이 없는 경우, 여러 스레드가 동시에 여러 변수를 읽고 쓸 때, 한 스레드는 다른 스레드가 값을 기록한 순서와 다른 순서로 값이 변경되는 것을 관찰할 수 있습니다. 실제로 변경 사항의 명백한 순서는 여러 독자 스레드 간에도 서로 다를 수 있습니다. 메모리 모델이 허용하는 컴파일러 변환으로 인해 유니프로세서 시스템에서도 유사한 효과가 발생할 수 있습니다.

라이브러리의 모든 원자적 연산의 기본 동작은 순차적 일관성 순서 를 제공합니다(아래 논의 참조). 이 기본값은 성능에 영향을 미칠 수 있지만, 라이브러리의 원자적 연산에 추가적인 std::memory_order 인수를 지정하여 원자성 이상의 정확한 제약 조건을 컴파일러와 프로세서가 해당 연산에 대해 강제하도록 할 수 있습니다.

목차

상수

헤더 파일에 정의됨 <atomic>
이름 의미
memory_order_relaxed 완화된 연산: 다른 읽기나 쓰기 작업에 대한 동기화나 순서 제약을 부과하지 않으며, 이 연산의 원자성만 보장됨 (아래 완화된 순서 참조).
memory_order_consume
(C++26에서 사용 중단됨)
이 메모리 순서를 사용하는 로드 연산은 영향을 받는 메모리 위치에 대해 소비 연산 을 수행함: 현재 로드되는 값에 의존하는 현재 스레드의 읽기나 쓰기 작업이 이 로드보다 앞서 재배치되지 않음. 동일한 원자 변수를 해제하는 다른 스레드에서 데이터 의존적 변수에 대한 쓰기 작업이 현재 스레드에서 가시적임. 대부분의 플랫폼에서 이는 컴파일러 최적화에만 영향을 줌 (아래 해제-소비 순서 참조).
memory_order_acquire 이 메모리 순서를 사용하는 로드 연산은 영향을 받는 메모리 위치에 대해 획득 연산 을 수행함: 현재 스레드의 읽기나 쓰기 작업이 이 로드보다 앞서 재배치되지 않음. 동일한 원자 변수를 해제하는 다른 스레드의 모든 쓰기 작업이 현재 스레드에서 가시적임 (아래 해제-획득 순서 참조).
memory_order_release 이 메모리 순서를 사용하는 저장 연산은 해제 연산 을 수행함: 현재 스레드의 읽기나 쓰기 작업이 이 저장보다 뒤로 재배치되지 않음. 현재 스레드의 모든 쓰기 작업이 동일한 원자 변수를 획득하는 다른 스레드에서 가시적임 (아래 해제-획득 순서 참조) 그리고 원자 변수로 의존성을 가지는 쓰기 작업이 동일한 원자 변수를 소비하는 다른 스레드에서 가시적이 됨 (아래 해제-소비 순서 참조).
memory_order_acq_rel 이 메모리 순서를 사용하는 읽기-수정-쓰기 연산은 획득 연산 이자 해제 연산 임. 현재 스레드의 메모리 읽기나 쓰기 작업이 로드보다 앞서 재배치될 수 없으며, 저장보다 뒤로 재배치될 수도 없음. 동일한 원자 변수를 해제하는 다른 스레드의 모든 쓰기 작업이 수정 전에 가시적이며, 수정 작업은 동일한 원자 변수를 획득하는 다른 스레드에서 가시적임.
memory_order_seq_cst 이 메모리 순서를 사용하는 로드 연산은 획득 연산 을 수행하고, 저장 연산은 해제 연산 을 수행하며, 읽기-수정-쓰기 연산은 획득 연산 해제 연산 을 모두 수행함. 또한 모든 스레드가 동일한 순서로 모든 수정 사항을 관찰하는 단일 전체 순서가 존재함 (아래 순차적 일관성 순서 참조).

공식 설명

스레드 간 동기화와 메모리 순서는 서로 다른 실행 스레드들 사이에서 표현식의 평가(evaluations) 부수 효과(side effects) 가 어떻게 순서화되는지를 결정합니다. 이들은 다음 용어들로 정의됩니다:

Sequenced-before

동일한 스레드 내에서, 평가 A는 평가 순서 에 설명된 대로 평가 B보다 sequenced-before 될 수 있습니다.

의존성 전달

동일한 스레드 내에서 평가 A가 평가 B보다 순서 상 앞서는 경우 (sequenced-before), 다음 중 어느 하나에 해당하면 A가 B로 의존성을 전달할 수 있습니다(즉, B가 A에 의존함):

1) A의 값이 B의 피연산자로 사용되는 경우,
a) B가 std::kill_dependency 호출인 경우,
b) A가 내장 && , || , ?: , 또는 , 연산자의 왼쪽 피연산자인 경우.
2) A가 스칼라 객체 M에 쓰기를 하고, B가 M에서 읽기를 하는 경우.
3) A가 다른 평가 X로 의존성을 전달하고, X가 B로 의존성을 전달하는 경우.
(C++26까지)

수정 순서

특정 원자 변수에 대한 모든 수정은 해당 원자 변수에 특화된 전체 순서로 발생합니다.

모든 원자적 연산에 대해 다음 네 가지 요구 사항이 보장됩니다:

1) Write-write coherence : 원자적 객체 M을 수정하는 평가 A(쓰기 작업)가 M을 수정하는 평가 B보다 happens-before 관계에 있다면, A는 M의 modification order 에서 B보다 앞서 나타납니다.
2) 읽기-읽기 일관성 : 어떤 원자적 객체 M에 대한 값 계산 A(읽기)가 M에 대한 값 계산 B보다 happens-before 관계에 있고, A의 값이 M에 대한 쓰기 X에서 비롯된 경우, B의 값은 X에 의해 저장된 값이거나 M의 modification order 에서 X보다 나중에 나타나는 M에 대한 부수 효과 Y에 의해 저장된 값이어야 합니다.
3) 읽기-쓰기 일관성 : 어떤 원자적 객체 M의 값 계산 A(읽기)가 M에 대한 연산 B(쓰기)에 대해 happens-before 관계에 있다면, A의 값은 M의 modification order 에서 B보다 앞서 나타나는 부수 효과(쓰기) X로부터 비롯된다.
4) 쓰기-읽기 일관성 : 원자적 객체 M에 대한 부수 효과(쓰기) X가 M의 값 계산(읽기) B에 happens-before 관계에 있다면, 평가 B는 X로부터 또는 M의 수정 순서에서 X 뒤에 오는 부수 효과 Y로부터 그 값을 취해야 한다.

릴리스 시퀀스

원자적 객체 M에 대해 release 연산 A가 수행된 후, M의 수정 순서 중 다음으로 구성된 가장 긴 연속 부분 순서:

1) A를 수행한 동일 스레드가 수행한 쓰기 작업.
(until C++20)
2) M에 대해 모든 스레드가 수행하는 원자적 읽기-수정-쓰기 연산.

A에 의해 시작된 릴리스 시퀀스 로 알려져 있습니다.

동기화 대상

스레드 A의 원자적 저장 연산이 release operation 이고, 동일한 변수에 대한 스레드 B의 원자적 로드 연산이 acquire operation 이며, 스레드 B의 로드 연산이 스레드 A의 저장 연산이 기록한 값을 읽는 경우, 스레드 A의 저장 연산은 스레드 B의 로드 연산과 synchronizes-with 관계를 형성합니다.

또한, 일부 라이브러리 호출은 다른 스레드의 다른 라이브러리 호출과 synchronize-with 되도록 정의될 수 있습니다.

의존성 순서 이전

스레드 간에, 다음 중 하나가 참인 경우 평가 A는 평가 B에 대해 의존성 순서 이전 관계에 있습니다:

1) A가 어떤 원자적 객체 M에 대해 해제 연산 을 수행하고, 다른 스레드에서 B가 동일한 원자적 객체 M에 대해 소비 연산 을 수행하며, B가 A에 의해 쓰여진 값을 읽는 경우 (해제 시퀀스의 일부로) (C++20까지)
2) A가 X에 대해 의존성 순서 이전 관계에 있고 X가 B로 의존성을 전달하는 경우
(C++26까지)

스레드 간 happens-before

스레드 간에, 평가 A가 inter-thread happens before 평가 B인 경우는 다음 중 하나가 참일 때입니다:

1) A가 synchronizes-with B.
2) A는 dependency-ordered before B입니다.
3) A가 synchronizes-with 일부 평가 X와 동기화되고, X가 B에 sequenced-before 됩니다.
4) A는 sequenced-before 일부 평가 X이고, X는 inter-thread happens-before B입니다.
5) A가 inter-thread happens-before 일부 평가 X에 해당하고, X가 inter-thread happens-before B에 해당합니다.


Happens-before

스레드와 무관하게, 다음 중 하나라도 참이면 평가 A가 평가 B에 대해 happens-before 관계에 있습니다:

1) A가 B에 대해 sequenced-before 관계에 있는 경우.
2) A가 B에 대해 inter-thread happens before 관계에 있는 경우.

구현체는 (필요한 경우 추가적인 동기화를 도입하여) happens-before 관계가 비순환적이도록 보장해야 합니다 (이것은 consume 연산이 관련된 경우에만 필요할 수 있으며, Batty et al 참조).

한 평가가 메모리 위치를 수정하고 다른 평가가 동일한 메모리 위치를 읽거나 수정하며, 두 평가 중 적어도 하나가 원자 연산이 아닌 경우, 이 두 평가 사이에 happens-before 관계가 존재하지 않으면 프로그램의 동작은 정의되지 않습니다 (프로그램에 데이터 경쟁 이 있습니다).

Simply happens-before

스레드와 무관하게, 다음 중 하나라도 참이면 평가 A가 평가 B에 대해 simply happens-before 관계에 있습니다:

1) A가 B에 대해 sequenced-before 관계에 있는 경우.
2) A가 B에 대해 synchronizes-with 관계에 있는 경우.
3) A가 X에 대해 simply happens-before 관계에 있고 X가 B에 대해 simply happens-before 관계에 있는 경우.

참고: consume 연산이 없는 경우, simply happens-before 관계와 happens-before 관계는 동일합니다.

(C++20부터)
(C++26까지)

Happens-before

스레드와 무관하게, 다음 중 하나라도 참이면 평가 A가 평가 B에 대해 happens-before 관계에 있습니다:

1) A가 B에 대해 sequenced-before 관계에 있는 경우.
2) A가 B에 대해 synchronizes-with 관계에 있는 경우.
3) A가 X에 대해 happens-before 관계에 있고 X가 B에 대해 happens-before 관계에 있는 경우.
(C++26부터)

강력하게 선행(Strongly happens-before)

스레드와 관계없이, 다음 중 하나라도 참이면 평가 A가 평가 B에 대해 강하게 선행 발생(strongly happens-before) 합니다:

1) A가 B보다 sequenced-before 됩니다.
2) A가 B와 synchronizes-with 합니다.
3) A가 X보다 strongly happens-before 하고, X가 B보다 strongly happens-before 합니다.
(C++20 이전)
1) A가 B보다 sequenced-before 됩니다.
2) A가 B와 synchronizes with 하고, A와 B 모두 순차적 일관성 원자 연산입니다.
3) A가 X보다 sequenced-before 되고, X가 Y보다 단순히 (C++26 이전) happens-before 하고, Y가 B보다 sequenced-before 됩니다.
4) A가 X보다 strongly happens-before 하고, X가 B보다 strongly happens-before 합니다.

참고: 비공식적으로, A가 B보다 strongly happens-before 하면, A는 모든 컨텍스트에서 B보다 먼저 평가되는 것으로 보입니다.

참고: strongly happens-before 는 consume 연산을 제외합니다.

(C++26 이전)
(C++20 이후)

가시적 부작용

스칼라 M에 대한 부작용 A(쓰기)는 다음 두 조건이 모두 참일 때 M에 대한 값 계산 B(읽기)에 대해 가시적 입니다:

1) A가 happens-before B입니다.
2) A가 X에 대해 happens-before 관계이고 X가 B에 대해 happens-before 관계인 다른 부작용 X가 M에 존재하지 않는다.

부작용 A가 값 계산 B와 관련하여 가시적이라면, M에 대한 부작용들 중 가장 긴 연속된 부분집합으로서, 수정 순서 상에서 B가 그것에 선행하지 않는 부분을 가시적 부작용 시퀀스 라고 합니다 (B에 의해 결정되는 M의 값은 이러한 부작용 중 하나에 의해 저장된 값이 됩니다).

참고: 스레드 간 동기화는 데이터 경쟁(선행 관계를 설정하여)을 방지하고 어떤 조건에서 어떤 부작용이 표시되는지 정의하는 것으로 귀결됩니다.

소비 연산

memory_order_consume 또는 그 이상의 메모리 순서를 사용하는 원자적 로드는 소비 연산(consume operation)입니다. std::atomic_thread_fence 는 소비 연산보다 더 강력한 동기화 요구 사항을 부과한다는 점에 유의하십시오.

Acquire 연산

memory_order_acquire 또는 그 이상의 메모리 순서를 사용한 원자적 로드는 획득 연산입니다. Mutex lock() 연산 또한 획득 연산입니다. std::atomic_thread_fence 는 획득 연산보다 더 강력한 동기화 요구사항을 부과한다는 점에 유의하십시오.

릴리스 연산

memory_order_release 또는 그보다 강한 메모리 순서를 사용한 원자적 저장(atomic store)은 릴리스 연산입니다. Mutex unlock() 연산 또한 릴리스 연산입니다. std::atomic_thread_fence 는 릴리스 연산보다 더 강력한 동기화 요구사항을 부과한다는 점에 유의하십시오.

설명

릴렉스드 오더링

memory_order_relaxed 태그가 지정된 원자 연산은 동기화 연산이 아닙니다; 이들은 동시적 메모리 접근 간에 순서를 부과하지 않습니다. 오직 원자성과 수정 순서 일관성만을 보장합니다.

예를 들어, x y 가 초기에 0인 경우,

// 스레드 1:
r1 = y.load(std::memory_order_relaxed); // A
x.store(r1, std::memory_order_relaxed); // B
// 스레드 2:
r2 = x.load(std::memory_order_relaxed); // C 
y.store(42, std::memory_order_relaxed); // D

r1 == r2 == 42 가 허용되는 이유는, 스레드 1 내에서 A가 B보다 sequenced-before 관계에 있고 스레드 2 내에서 C가 D보다 sequenced before 관계에 있지만, y 의 수정 순서에서 D가 A보다 먼저 나타나는 것을 막는 것은 없으며, x 의 수정 순서에서 B가 C보다 먼저 나타나는 것을 막는 것도 없기 때문입니다. y 에 대한 D의 부작용이 스레드 1의 로드 A에 보일 수 있고, x 에 대한 B의 부작용이 스레드 2의 로드 C에 보일 수 있습니다. 특히, 컴파일러 재배치나 런타임 중에 스레드 2에서 C보다 D가 먼저 완료되는 경우 이런 현상이 발생할 수 있습니다.

완화된 메모리 모델에서도, 허공에서 나타난 값(Out-of-thin-air)이 자체 계산에 순환적으로 의존하는 것은 허용되지 않습니다. 예를 들어, x y 가 초기에 0인 경우,

// Thread 1:
r1 = y.load(std::memory_order_relaxed);
if (r1 == 42)
    x.store(r1, std::memory_order_relaxed);
// Thread 2:
r2 = x.load(std::memory_order_relaxed);
if (r2 == 42)
    y.store(42, std::memory_order_relaxed);

이 코드가 r1 == r2 == 42 를 생성하는 것은 허용되지 않습니다. 왜냐하면 y 42 를 저장하는 것은 x 42 를 저장하는 경우에만 가능하며, 이는 y 42 를 저장하는 것에 순환적으로 의존하기 때문입니다. C++14 이전에는 명세상 기술적으로 허용되었지만, 구현자들에게 권장되지 않았습니다.

(since C++14)

완화된 메모리 순서의 일반적인 사용 예는 참조 카운터와 같은 카운터 증분입니다. std::shared_ptr 의 참조 카운터가 이에 해당합니다. 이는 원자성만 필요로 하고 순서나 동기화는 필요하지 않기 때문입니다(단, std::shared_ptr 카운터 감소는 소멸자와의 획득-해제 동기화가 필요함에 유의하세요).

#include <atomic>
#include <iostream>
#include <thread>
#include <vector>
std::atomic<int> cnt = {0};
void f()
{
    for (int n = 0; n < 1000; ++n)
        cnt.fetch_add(1, std::memory_order_relaxed);
}
int main()
{
    std::vector<std::thread> v;
    for (int n = 0; n < 10; ++n)
        v.emplace_back(f);
    for (auto& t : v)
        t.join();
    std::cout << "Final counter value is " << cnt << '\n';
}

출력:

Final counter value is 10000

릴리스-어퀴어 순서 지정

스레드 A의 원자적 저장 연산이 memory_order_release 로 태그되고, 동일한 변수에 대한 스레드 B의 원자적 로드 연산이 memory_order_acquire 로 태그되며, 스레드 B의 로드 연산이 스레드 A의 저장 연산이 기록한 값을 읽는 경우, 스레드 A의 저장 연산은 스레드 B의 로드 연산과 동기화됩니다(synchronizes-with) .

스레드 A의 관점에서 원자적 저장 이전에 happened-before 관계에 있는 모든 메모리 기록(비원자적 및 완화된 원자적 기록 포함)은 스레드 B에서 visible side-effects 가 됩니다. 즉, 원자적 로드가 완료되면 스레드 B는 스레드 A가 메모리에 기록한 모든 내용을 보게 됩니다. 이 약속은 B가 실제로 A가 저장한 값이나 릴리스 시퀀스에서 이후의 값을 반환하는 경우에만 유효합니다.

동기화는 동일한 원자 변수를 해제하는 스레드와 획득하는 스레드 사이에서만 확립됩니다. 다른 스레드들은 동기화된 스레드 중 하나 또는 둘 모두와 다른 메모리 접근 순서를 관찰할 수 있습니다.

강력하게 정렬된 시스템(x86, SPARC TSO, IBM 메인프레임 등)에서는 대부분의 연산에 대해 릴리스-획득 순서가 자동으로 적용됩니다. 이 동기화 모드에는 추가적인 CPU 명령어가 발생하지 않으며, 특정 컴파일러 최적화만 영향을 받습니다(예: 컴파일러는 비원자적 저장을 원자적 저장-릴리스 이후로 이동하거나 비원자적 로드를 원자적 로드-획득보다 앞서 수행하는 것이 금지됩니다). 약하게 정렬된 시스템(ARM, Itanium, PowerPC)에서는 특수 CPU 로드 또는 메모리 펜스 명령어가 사용됩니다.

상호 배제 락, 예를 들어 std::mutex 또는 atomic spinlock 은 릴리스-어퀴어 동기화의 예시입니다: 스레드 A가 락을 릴리스하고 스레드 B가 어퀴어할 때, 스레드 A의 컨텍스트에서 크리티컬 섹션(릴리스 전)에서 발생한 모든 것은 동일한 크리티컬 섹션을 실행하는 스레드 B(어퀴어 후)에게 가시적이어야 합니다.

#include <atomic>
#include <cassert>
#include <string>
#include <thread>
std::atomic<std::string*> ptr;
int data;
void producer()
{
    std::string* p = new std::string("Hello");
    data = 42;
    ptr.store(p, std::memory_order_release);
}
void consumer()
{
    std::string* p2;
    while (!(p2 = ptr.load(std::memory_order_acquire)))
        ;
    assert(*p2 == "Hello"); // 절대 발생하지 않음
    assert(data == 42); // 절대 발생하지 않음
}
int main()
{
    std::thread t1(producer);
    std::thread t2(consumer);
    t1.join(); t2.join();
}

다음 예제는 릴리스 시퀀스를 사용하여 세 개의 스레드에 걸친 전이적 릴리스-획득 순서 지정을 보여줍니다.

#include <atomic>
#include <cassert>
#include <thread>
#include <vector>
std::vector<int> data;
std::atomic<int> flag = {0};
void thread_1()
{
    data.push_back(42);
    flag.store(1, std::memory_order_release);
}
void thread_2()
{
    int expected = 1;
    // memory_order_relaxed is okay because this is an RMW,
    // and RMWs (with any ordering) following a release form a release sequence
    while (!flag.compare_exchange_strong(expected, 2, std::memory_order_relaxed))
    {
        expected = 1;
    }
}
void thread_3()
{
    while (flag.load(std::memory_order_acquire) < 2)
        ;
    // if we read the value 2 from the atomic flag, we see 42 in the vector
    assert(data.at(0) == 42); // will never fire
}
int main()
{
    std::thread a(thread_1);
    std::thread b(thread_2);
    std::thread c(thread_3);
    a.join(); b.join(); c.join();
}

릴리스-컨슘 순서화

스레드 A의 원자적 저장 연산이 memory_order_release 로 태그되고, 스레드 B의 동일 변수에 대한 원자적 로드 연산이 memory_order_consume 로 태그되며, 스레드 B의 로드 연산이 스레드 A의 저장 연산이 기록한 값을 읽는 경우, 스레드 A의 저장 연산은 스레드 B의 로드 연산에 대해 의존성 순서로 선행 합니다.

스레드 A의 관점에서 원자적 저장 연산에 발생-이전 한 모든 메모리 기록(비원자적 및 완화된 원자적)은 스레드 B에서 로드 연산이 의존성을 전달 하는 연산들 내에서 부작용 가시화 가 됩니다. 즉, 원자적 로드가 완료되면 스레드 B에서 로드로부터 획득한 값을 사용하는 연산자들과 함수들은 스레드 A가 메모리에 기록한 내용을 반드시 보게 됩니다.

동기화는 동일한 원자 변수를 해제 하는 스레드와 소비 하는 스레드 사이에서만 확립됩니다. 다른 스레드들은 동기화된 스레드들 중 하나 또는 둘 모두와 다른 메모리 접근 순서를 관찰할 수 있습니다.

DEC Alpha를 제외한 모든 주류 CPU에서 의존성 순서는 자동으로 적용되며, 이 동기화 모드에 대해 추가적인 CPU 명령어가 발생하지 않습니다. 특정 컴파일러 최적화만 영향을 받습니다(예: 컴파일러는 의존성 체인에 관여하는 객체들에 대한 추측적 로드를 수행하는 것이 금지됨).

이 순서의 일반적인 사용 사례는 자주 기록되지 않는 동시성 데이터 구조(라우팅 테이블, 설정, 보안 정책, 방화벽 규칙 등)에 대한 읽기 접근과 포인터 매개 발행을 통한 발행-구독 상황을 포함합니다. 즉, 생산자가 소비자가 정보에 접근할 수 있는 포인터를 발행할 때: 생산자가 메모리에 기록한 다른 모든 내용을 소비자에게 가시화할 필요가 없습니다(약한 순서 아키텍처에서 비용이 큰 연산일 수 있음). 이러한 시나리오의 예시로 rcu_dereference 가 있습니다.

세분화된 의존성 체인 제어를 위해 std::kill_dependency [[ carries_dependency ]] 도 참조하십시오.

현재(2015년 2월) 알려진 상용 컴파일러들은 의존성 체인을 추적하지 않습니다: consume 연산들은 acquire 연산으로 승격됩니다.

(until C++26)

릴리스-컨슘 순서 지정 규격이 개정 중이며, memory_order_consume 사용이 일시적으로 권장되지 않습니다.

(C++17부터)
(C++26까지)

릴리스-컨슘 순서 지정은 릴리스-어퀴어 순서 지정과 동일한 효과를 가지며 사용이 권장되지 않습니다.

(C++26부터)

이 예제는 포인터를 통한 발행에 대한 의존성 순서 동기화를 보여줍니다: 정수 데이터는 문자열 포인터와 데이터 의존성 관계로 연결되어 있지 않으므로, 소비자 측에서 그 값은 정의되지 않습니다.

#include <atomic>
#include <cassert>
#include <string>
#include <thread>
std::atomic<std::string*> ptr;
int data;
void producer()
{
    std::string* p = new std::string("Hello");
    data = 42;
    ptr.store(p, std::memory_order_release);
}
void consumer()
{
    std::string* p2;
    while (!(p2 = ptr.load(std::memory_order_consume)))
        ;
    assert(*p2 == "Hello"); // never fires: *p2 carries dependency from ptr
    assert(data == 42); // may or may not fire: data does not carry dependency from ptr
}
int main()
{
    std::thread t1(producer);
    std::thread t2(consumer);
    t1.join(); t2.join();
}


순차적 일관성 순서

memory_order_seq_cst 태그로 지정된 원자적 연산들은 릴리스/어quire 순서와 동일한 방식으로 메모리를 정렬할 뿐만 아니라(한 스레드에서 저장 연산 이전에 발생한 모든 것 이 로드를 수행한 스레드에서 가시적인 부수 효과 가 됨), 이렇게 태그된 모든 원자적 연산들의 단일 총 수정 순서 를 확립합니다.

공식적으로,

원자 변수 M에서 로드하는 각 memory_order_seq_cst 연산 B는 다음 중 하나를 관찰합니다:

  • 단일 전체 순서에서 B 이전에 나타나는 M을 수정한 마지막 연산 A의 결과,
  • 또는, 그러한 A가 존재하는 경우 B는 memory_order_seq_cst 가 아니고 A에 happen-before 관계가 없는 M의 어떤 수정 결과를 관찰할 수 있습니다,
  • 또는, 그러한 A가 존재하지 않는 경우 B는 memory_order_seq_cst 가 아닌 M의 관련 없는 수정 결과를 관찰할 수 있습니다.

B보다 sequenced-before memory_order_seq_cst std::atomic_thread_fence 연산 X가 존재하는 경우, B는 다음 중 하나를 관찰합니다:

  • 단일 전체 순서에서 X 이전에 나타나는 M의 마지막 memory_order_seq_cst 수정,
  • M의 수정 순서에서 나중에 나타나는 M의 관련 없는 수정.

M에 대한 원자 연산 쌍 A와 B(A는 쓰기, B는 M의 값을 읽음)에 대해, 두 개의 memory_order_seq_cst std::atomic_thread_fence X와 Y가 존재하고, A가 X에 sequenced-before 되고, Y가 B에 sequenced-before 되며, 단일 전체 순서에서 X가 Y보다 먼저 나타나는 경우, B는 다음 중 하나를 관찰합니다:

  • A의 효과,
  • M의 수정 순서에서 A 이후에 나타나는 M의 관련 없는 수정.

M의 원자 수정 쌍 A와 B에 대해, 다음 조건에서 B는 M의 수정 순서에서 A 이후에 발생합니다:

  • A가 sequenced-before 되고 단일 전체 순서에서 B보다 먼저 나타나는 memory_order_seq_cst std::atomic_thread_fence X가 존재하는 경우,
  • 또는, Y가 B에 sequenced-before 되고 단일 전체 순서에서 A가 Y보다 먼저 나타나는 memory_order_seq_cst std::atomic_thread_fence Y가 존재하는 경우,
  • 또는, A가 X에 sequenced-before 되고 Y가 B에 sequenced-before 되며 단일 전체 순서에서 X가 Y보다 먼저 나타나는 memory_order_seq_cst std::atomic_thread_fence s X와 Y가 존재하는 경우.

이는 다음을 의미합니다:

1) memory_order_seq_cst 로 태그되지 않은 원자 연산이 등장하면 순차적 일관성이 손실됩니다,
2) 순차적 일관성 펜스는 일반적인 경우 원자 연산 전체가 아닌 펜스 자체의 전체 순서만 확립합니다 ( sequenced-before happens-before 와 달리 스레드 간 관계가 아닙니다).
(until C++20)
공식적으로,

어떤 원자 객체 M에 대한 원자 연산 A가 다른 원자 연산 B에 대해 coherence-ordered-before 관계를 가지는 경우는 다음 중 하나가 참일 때입니다:

1) A가 수정(modification)이고, B가 A에 의해 저장된 값을 읽는 경우,
2) A가 M의 modification order 에서 B보다 앞서는 경우,
3) A가 원자 수정 X에 의해 저장된 값을 읽고, X가 modification order 에서 B보다 앞서며, A와 B가 동일한 원자 read-modify-write 연산이 아닌 경우,
4) A가 X에 대해 coherence-ordered-before 이고, X가 B에 대해 coherence-ordered-before 인 경우.

모든 memory_order_seq_cst 연산(펜스를 포함)에 대해 단일 전체 순서 S가 존재하며, 이는 다음 제약 조건을 만족합니다:

1) A와 B가 memory_order_seq_cst 연산이고 A가 B에 대해 strongly happens-before 라면, S에서 A가 B보다 앞섭니다,
2) 객체 M에 대한 원자 연산 A와 B의 쌍에 대해, A가 B에 대해 coherence-ordered-before 인 경우:
a) A와 B가 모두 memory_order_seq_cst 연산이면, S에서 A가 B보다 앞섭니다,
b) A가 memory_order_seq_cst 연산이고, B가 memory_order_seq_cst 펜스 Y에 대해 happens-before 라면, S에서 A가 Y보다 앞섭니다,
c) memory_order_seq_cst 펜스 X가 A에 대해 happens-before 이고, B가 memory_order_seq_cst 연산이면, S에서 X가 B보다 앞섭니다,
d) memory_order_seq_cst 펜스 X가 A에 대해 happens-before 이고, B가 memory_order_seq_cst 펜스 Y에 대해 happens-before 라면, S에서 X가 Y보다 앞섭니다.

이 공식 정의는 다음을 보장합니다:

1) 단일 전체 순서가 모든 원자 객체의 modification order 와 일관됩니다,
2) memory_order_seq_cst 로드가 자신의 값을 마지막 memory_order_seq_cst 수정으로부터 얻거나, 선행하는 memory_order_seq_cst 수정들에 대해 happen-before 관계가 없는 비- memory_order_seq_cst 수정으로부터 얻습니다.

단일 전체 순서는 happens-before 와 일관되지 않을 수 있습니다. 이는 일부 CPU에서 memory_order_acquire memory_order_release 의 더 효율적인 구현을 허용합니다. memory_order_acquire memory_order_release memory_order_seq_cst 와 혼합될 때 놀라운 결과를 생성할 수 있습니다.

예를 들어, x y 가 처음에 0인 경우,

// Thread 1:
x.store(1, std::memory_order_seq_cst); // A
y.store(1, std::memory_order_release); // B
// Thread 2:
r1 = y.fetch_add(1, std::memory_order_seq_cst); // C
r2 = y.load(std::memory_order_relaxed); // D
// Thread 3:
y.store(3, std::memory_order_seq_cst); // E
r3 = x.load(std::memory_order_seq_cst); // F

다음 결과를 생성할 수 있습니다: r1 == 1 && r2 == 3 && r3 == 0 , 여기서 A가 C에 대해 happens-before 이지만, C가 memory_order_seq_cst 의 단일 전체 순서 C-E-F-A에서 A보다 앞섭니다( Lahav et al 참조).

다음을 유의하세요:

1) memory_order_seq_cst 로 태그되지 않은 원자 연산이 등장하는 순간, 프로그램에 대한 순차적 일관성 보장은 사라집니다,
2) 많은 경우, memory_order_seq_cst 원자 연산은 동일한 스레드에서 수행되는 다른 원자 연산에 대해 재정렬 가능합니다.
(C++20부터)

순차적 순서 지정은 모든 소비자가 모든 생산자의 동작이 동일한 순서로 발생하는 것을 관찰해야 하는 다중 생산자-다중 소비자 상황에서 필요할 수 있습니다.

전체 순차적 순서화는 모든 멀티코어 시스템에서 완전한 메모리 펜스 CPU 명령어를 필요로 합니다. 이는 영향을 받는 메모리 접근들이 모든 코어로 전파되도록 강제하기 때문에 성능 병목 현상이 될 수 있습니다.

이 예제는 순차적 순서 지정이 필요한 상황을 보여줍니다. 다른 순서 지정 방식에서는 스레드 c d 가 원자 변수 x y 의 변경 사항을 서로 반대 순서로 관찰할 가능성이 있기 때문에 assert가 발생할 수 있습니다.

#include <atomic>
#include <cassert>
#include <thread>
std::atomic<bool> x = {false};
std::atomic<bool> y = {false};
std::atomic<int> z = {0};
void write_x()
{
    x.store(true, std::memory_order_seq_cst);
}
void write_y()
{
    y.store(true, std::memory_order_seq_cst);
}
void read_x_then_y()
{
    while (!x.load(std::memory_order_seq_cst))
        ;
    if (y.load(std::memory_order_seq_cst))
        ++z;
}
void read_y_then_x()
{
    while (!y.load(std::memory_order_seq_cst))
        ;
    if (x.load(std::memory_order_seq_cst))
        ++z;
}
int main()
{
    std::thread a(write_x);
    std::thread b(write_y);
    std::thread c(read_x_then_y);
    std::thread d(read_y_then_x);
    a.join(); b.join(); c.join(); d.join();
    assert(z.load() != 0); // will never happen
}

volatile 과의 관계

실행 스레드 내에서, volatile glvalues 를 통한 접근(읽기 및 쓰기)은 동일한 스레드 내에서 sequenced-before 또는 sequenced-after 관계에 있는 관찰 가능한 사이드 이펙트(다른 volatile 접근 포함)를 기준으로 재배열될 수 없습니다. 그러나 이 순서는 volatile 접근이 스레드 간 동기화를 설정하지 않기 때문에 다른 스레드에서 관찰된다는 보장은 없습니다.

또한, volatile 접근은 원자적이지 않으며 (동시 읽기 및 쓰기는 data race 입니다) 메모리 순서를 정하지 않습니다 (비-volatile 메모리 접근들은 volatile 접근 주변에서 자유롭게 재정렬될 수 있습니다).

주목할 만한 예외는 Visual Studio로, 기본 설정에서는 모든 volatile 쓰기가 릴리스 의미를 가지고 모든 volatile 읽기가 획득 의미를 가집니다 ( Microsoft Docs ). 따라서 volatile 변수는 스레드 간 동기화에 사용될 수 있습니다. 표준 volatile 의미론은 다중 스레드 프로그래밍에 적용할 수 없지만, 동일한 스레드에서 실행되는 std::signal 핸들러와의 통신에는 sig_atomic_t 변수에 적용될 때 충분합니다. 컴파일러 옵션 /volatile:iso 를 사용하면 표준과 일관된 동작으로 복원할 수 있으며, 이는 대상 플랫폼이 ARM일 때 기본 설정입니다.

참고 항목

C 문서 참조: memory order

외부 링크

1. MOESI 프로토콜
2. x86-TSO: x86 멀티프로세서를 위한 엄격하고 실용적인 프로그래머 모델 P. Sewell 외, 2010
3. ARM 및 POWER 완화 메모리 모델에 대한 튜토리얼 소개 P. Sewell 외, 2012
4. MESIF: 점대점 상호 연결을 위한 2-홉 캐시 일관성 프로토콜 J.R. Goodman, H.H.J. Hum, 2009
5. 메모리 모델 Russ Cox, 2021