acquire와 release에 대한 똥글 2개를 썼었는데, 이번에는 이 둘을 이용해서 동기화하는 방법을 정리하려 해.


CPU0은 value에 값을 쓴 후 ready에 true를 쓰면

CPU1은 ready가 true가 될 때까지 기다렸다가 value를 읽어오는 다음과 같은 코드를 보자.


int value = 0;

bool ready = false;


void cpu0() {

  value = 3;

  ready = true;

}


void cpu1() {

  while(ready == false) { }

  int d = value;

  assert(d == 3); // it can be fired

}


언뜻 보기에 문제 없이 동작할 것 같지만, 실제로는 저 assert문이 fire될 수 있어. ready가 true임에도 불구하고 value는 3이 아닌 값이 읽힐 수 있는거지.


이유는 앞선 두 똥글에서 설명한 것과 같아. 실제로 메모리에 데이터가 쓰이는 순서와 읽는 순서가 의도와는 달라질 수 있기 때문이야.


이를 해결하려면 acquire/release를 이용해서 해결할 수 있어.


int value = 0;

std::atomic<bool> ready(false);


void cpu0() {

  value = 3;

  ready.store(true, std::memory_order_release);

}


void cpu1() {

  while(ready.load(std::memory_order_acquire) == false) { }

  int d = value;

  assert(d == 3); // it never fires인

}


cpu0에서는 value에 3이 쓰인 에 ready에 true를 쓰고, cpu1에서는 ready에 true가 읽힌 에 value를 읽어오니 d는 항상 3이 됨을 보장할 수 있지.


그럼 이제 싱글톤 패턴에서 흔히 사용되는 Double-checked locking pattern(이하 DCLP)가 C++에서 왜 문제가 될 수 있는지 보고 해결 해볼까 해.


다음은 흔히 사용되는 DCLP 코드야.


my_class *instance = nullptr;


my_class* get_instance() {

  if(!instance) {     // if instance is NULL

    lock_the_lock();

    if(!instance) {   // if instance is still NULL

      instance = new my_class();

    }

    unlock_the_lock();

  }

  return instance;

}


이 코드 역시 언뜻 보기에 문제가 없을 것 같지만 사실은 쓰레드에 안전하지 않아.


instance = new my_class(); 이 문장은 크게 3가지 일을 수행해. 1) 메모리를 할당, 2) 할당된 메모리에 객체를 초기화, 3) 할당된 메모리 주소를 instance에 대입.


그런데 문제는, 할당된 메모리와 instance가 별개의 공간이기 때문에 cpu가 (객체 초기화, STORE B)와 (주소 대입, STORE A)을 순서를 바꿔버릴 수 있다는거야.


그림으로 그려보면 다음과 같아.


A B ─┬─┬─

│instance = nullptr│할당된 공간

│ ^



객체가 초기화되기 전에 instance이 NULL이 아닌 값으로 읽힐 수 있고, 따라서 초기화되지 않은 객체에 접근할 가능성이 생기는거지.


그러니까 객체 초기화와 instance에 주소를 대입하는 두 과정의 순서를 명시해줄 필요가 있어.


B에 쓴  A에 쓰도록 하는, 즉 객체가 생성된  instance에 주소가 쓰이도록 해줘야해.


acquire/release semantic을 이용해서 DCLP가 제대로 동작하게 만들면 다음과 같아.


std::atomice<my_class*> instance(nullptr);


my_class* get_instance() {

  my_class* p;

  if(!(p = instance.load(std::memory_order_acquire))) {     // if instance is NULL

    lock_the_lock();

    if(!(p = instance.load(std::memory_order_relaxed))) {   // if instance is still NULL

      p = new my_class();

      instance.store(p, std::memory_order_release);

    }

    unlock_the_lock();

  }

  return p;

}


acquire와 release를 이용해 객체초기화와 instance대입의 순서를 명시해줌으로써 이제는 쓰레드에 안전한 DCLP가 되었어.


중간에 사용된 memory_order_relaxed는 오더링이 필요없고 atomicity만 필요할 때 사용돼.


쉽게 정리한다고 하긴 했는데 갑자기 내용이 좀 복잡해진 느낌인데 어떻게 더 쉽게 써야할 지 잘 모르겠다 ㅎㅎ


acquire/release에 대해 정리하고 이를 이용한 동기화에 대해 정리해봤어. 다음에는 acquire의 특수한 경우인 memory_order_consume과 이를 이용해서 위에 사용된 DCLP를 좀 더 개선하는 내용을 정리해볼게.


좋은 하루 되길 바라.