저번에 (A에 쓴 후 B를 쓰는) release에 대해 썼는데 이번에는 (B를 읽은 후 A를 읽는) acquire에 대해 정리하려고 해.
─┬───┬─────┬───┬─
│A=0│ │B=0│
─┴───┴─────┴───┴─ ↑ ↑ CPU0 CPU1 (STORE B, 1) A를 읽은 후에, B를 읽음 (STORE A, 2, release)
B에다 1을 쓴 후 A에 2를 쓰는 CPU0이 있다고 하자. CPU1에서는 A를 읽은 후에 B를 읽고자 하는데, 다음과 같은 코드를 짤 경우 전 글에서와 마찬가지로 컴파일러, CPU 모두에서 리오더링이 일어날 수 있어.
int rA = A;
int rB = B;
(rA, rB)의 결과는 보통 (0, 0), (0, 1), (2, 1)이 될 것으로 예상하겠지만, 실제로는 (2, 0)이 될 수도 있어.
이유는 마찬가지로, 컴파일러가 다음과 같이 인스트럭션을 리오더링할 경우가 있겠고,
LOAD rB, B
LOAD rA, A // 소스 코드 상에는 A를 먼저 읽도록 했지만 A와 B가 다른 장소이므로 컴파일러가 임의로 리오더링할 수 있음
컴파일러가 리오더링을 하지 않을 경우에도 CPU 단에서 B를 먼저 관측한 값을 이용하고 A를 다음에 관측할 수도 있어.
그래서 release와 마찬가지로 메모리 오더를 명시할 필요가 있는데, 이 때 사용되는게 acquire semantic이야.
acquire semantic을 이용하면 다음과 같이 표현할 수 있어.
int rA = A, memory_order_acquire;
int rB = B;
이러면 A를 읽은(LOAD) 후에 B를 읽어옴(LOAD)을 보장할 수 있어.
마찬가지로 arm-gcc로 다음과 같은 코드를 컴파일해서 확인해보자.
#include <atomic>
std::atomic<int> A(0);
int B = 0;
void f(int *ra, int *rb)
{
*ra = A.load(std::memory_order_acquire);
*rb = B;
}
// 아래와 같이 짤 경우 ra와 rb가 사용되지 않기 때문에 컴파일러가 최적화를 통해 빈 함수 코드를 생성할 수도 있음
/*
void f()
{
int ra = A.load(std::memory_order_acquire);
int rb = B;
}
*/
(http://preshing.com/20120913/acquire-and-release-semantics 에서 가져옴)
즐주요