2016년 9월 29일 목요일

acquire / release fence의 핵심

NOTE: Preshing 블로그의 DCLP관련 포스팅의 요약입니다.

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 제약조건을 사용한 코드 형태.

댓글 없음:

댓글 쓰기