1. store-release
atomic_thread_fence(release)
atomic store(relaxed)
=>atomic store(release)
2. load-acquire
atomic load(relaxed)
atomic_thread_fence(acquire)
=>atomic load(acquire)
3. acquire semantics, release semantics
acquire semantics는 공유 메모리에서 읽기하는 작업에만 적용될 수 있는 속성, 그 읽기 작업을 read-acquire라 한다.
release semantics는 공유 메모리에 쓰기 작업에만 적용될 수 있는 속성, 그 쓰기 작업을 write-release라 한다.
acquire semantics는 프로그램 순서상 read-acquire와 그 이후의 어떠한 메모리 읽기/쓰기와 메모리 재배치를 막는다.
비슷하게 release-semantics는 프로그램 순서상 write-release와 그 이전의 어떠한 메모리 읽기/쓰기와 메모리 재배치를 막는다.
4. 이걸로 무엇을 할 수 있나
DCLP가 가장 직접적인 예일듯. 다음은 Preshing 블로그에 나와있는 예다.
singleton패턴의 getInstance() 함수다. 인스턴스를 포인터로 만드는 케이스.
Singleton* Singleton::getInstance() {
Lock lock; // scope-based lock, released automatically when the function returns
if (m_instance == NULL) {
m_instance = new Singleton;
}
return m_instance;
}
우리는 보다 나은 성능을 위해 Lock(mutex 같은)을 사용하지 않고 구현하려 한다.m_instance가 NULL인지 체크하는 부분은 getInstance()가 호출될 때마다 수행된다.
처음 m_instance를 생성할 때에는 실행 비용이 비싸도 되지만 한번 생성된 뒤에는 실행 비용이 적으면 적을수록 좋을 것. 이로부터 DCLP는 출발한다. 우선 이 기준으로 위 함수를 바꿔보면
Singleton* Singleton::getInstance() {
Singleton* tmp = m_instance;
... // insert memory barrier
if (tmp == NULL) {
Lock lock; // 아직 Lock은 있다. tmp가 NULL일때에만 Lock사용하므로 이 정도는 ok
tmp = m_instance;
if (tmp == NULL) {
tmp = new Singleton;
... // insert memory barrier
m_instance = tmp;
}
}
return tmp;
}
위에서 tmp 가 NULL이면 if문안에서 Lock을 건다. 그러고 나서 m_instance의 값을 다시한번 확인한다. 이는 처음 if문과 Lock 사이에 다른 쓰레드가 이미 getInstance()를 호출하고 Lock을 걸고 new를 했을 수도 있다. 다시한번 m_instance를 체크해서 여전히 NULL이라면new를 한다.
여기서 눈여겨 볼 부분은 tmp변수와 tmp = m_instance로 실제 공유 메모리인 m_instance의 값을 tmp로 복사해 오는 부분이다. 이는 barrier(fence)를 사용하기 위한 장치다.
std::atomic Singleton::m_instance;
std::mutex Singleton::m_mutex;
Singleton* Singleton::getInstance() {
Singleton* tmp = m_instance.load(std::memory_order_relaxed);
std::atomic_thread_fence(std::memory_order_acquire);
if (tmp == nullptr) {
std::lock_guard lock(m_mutex);
tmp = m_instance.load(std::memory_order_relaxed);
if (tmp == nullptr) {
tmp = new Singleton;
std::atomic_thread_fence(std::memory_order_release);
m_instance.store(tmp, std::memory_order_relaxed);
}
}
return tmp;
}
위 코드가 최종 모습. 자세한 설명은 해당 블로그를 참조하고 이로써 m_instance를 통해 쓰레드간에 synchronizes-with 관계를 형성한다. 매번 getInstance()마다 Lock걸 필요가 없으면서 쓰레드간에 안전하게 초기화할 수 있다.std::atomic Singleton::m_instance;
std::mutex Singleton::m_mutex;
Singleton* Singleton::getInstance() {
Singleton* tmp = m_instance.load(std::memory_order_acquire);
if (tmp == nullptr) {
std::lock_guard lock(m_mutex);
tmp = m_instance.load(std::memory_order_relaxed);
if (tmp == nullptr) {
tmp = new Singleton;
m_instance.store(tmp, std::memory_order_release);
}
}
return tmp;
}
C++11 atomic과 low level memory ordering 제약조건을 사용한 코드 형태.
댓글 없음:
댓글 쓰기