저번에 (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;

}

*/


arm-none-linux-gnueabi-g++ -Wall -Wextra -std=c++14 -pedantic -mthumb -mcpu=cortex-m4 -O2 a.cpp -S 명령을 통해 생성된 코드를 알기 쉽게 정리하면 다음과 같아.

ldr     rA, [A]
dmb     sy    // memory barrier instruction
ldr     rB, [B]

A를 읽고 난 후에 B를 읽도록 하는 것을 볼 수 있을거야.

정리하자면, release는 STORE 이전의 LOAD, STORE가 먼저 수행되도록 하는 것이고, acquire는 LOAD 이후의 LOAD, STORE가 나중에 수행되도록 하는거야.

그림으로 표현하면 다음과 같아.

LOAD, acquire // 아래쪽의 LOAD나 STORE가 위로 못올라옴
  LOAD
  STORE
  ...            // 얘네들 끼리는 리오더링이 가능함.
  LOAD
  STORE
STORE, release // 위쪽의 LOAD나 STORE가 아래로 못내려옴.

acquire는 LOAD operation에, release는 STORE operation에 사용돼.

또 다른 표현 방법으로는 다음과 같은 그림으로 표현할 수 있어.

 (http://preshing.com/20120913/acquire-and-release-semantics 에서 가져옴)

acquire는 LoadLoad, LoadStore 리오더링을 막고, release는 LoadStore, StoreStore 리오더링을 막기 위함이야.

StoreLoad 리오더링은 어떻게 막나 하는 의문이 생길 수 있는데, 이 경우는 full memory fence를 사용할 수 밖에 없어.


다음에 시간이 되면 acquire-release semantic을 이용해서 데이터를 동기화하는 과정과, 프겔러들이 좋아하는 싱글톤 패턴에 사용되는 Double-checked locking pattern이 왜 쓰레드에 안전하지 않은지, acquire-release를 이용해 쓰레드에 안전한 DCLP를 어떻게 구현하는지 정리해볼게.

좋은 주말 보내