(C++ : Concurrency) 12. memory order

Posted by : at

Category : Cpp


reorder

int a = 0;
int b = 0;

// thread A
void foo()
{
    a = b + 1;
    b = 1;
}

// thread B
void goo()
{
    if(b == 1)
    {
        // a == 1 을 보장할수 있을까?
        // 보장할순 없다.) 
        // 최적화 옵션에 따라 foo함수에서 b = 1이 먼저실행되기도 a = b + 1이 먼저 실행되기도 한다.
    }
}

왜 그렇게 될까?
결론부터 말하면 re-ordering때문이다.

void foo()
{
    a = b + 1;  // a를 쓰기위해서 b을 읽어와야한다.
    b = 1;      // 어차피 여기서 b에 1을 넣어야 하기에
    // a = b + 1;에서 b을 읽어왔을때 b를 먼저 1을 넣어버린다면? -> re-ordering
    // 컴파일러 나름대로 성능향상을 볼 수 있을것이다.
    // 이것이 문제가 된다.
}
  • 성능향상을 위해 코드의 실행순서를 변경하게 되는데
  • 컴파일, 실행시간에 발생하며 이로 인해서 생각지 못한 문제가 발생할 수 있다

해결책은 없나
fence이용

#include <atomic>

// ...

void foo()
{
    a = b + 1;
    
    // 위 코드가 아래로 넘어갈수 없다.
    std::atomic_thread_fence(std::memory_order_release);

    b = 1;
}

memory_order

#include <thread>

int x = 0;
int y = 0;

// foo, goo를 볼때 
// 1. 스레드 세이프한가(atomic operation 가능)
// 2. re-ordering으로 코드의 순서가 변경될 일이 없는가
// 를 중점으로 확인해 보자

void foo()
{
    // 보면 알겠지만 atomic하지도 re-ordering이 나타지 않을 것이란 보장도 없다.
    // 멀티스레드에서는 사용하면 안되는 코드이다.
    int n1 = y;
    x = n1;
}

void goo()
{
    int n2 = x;
    y = 100;
}

int main()
{
    std::thread t1(foo);
    std::thread t2(goo);
    t1.join(); t2.join();
}

해결해보자

#include <thread>
#include <atomic>

std::atomic<int> x = 0;
std::atomic<int> y = 0;

void foo()
{
    int n1 = y.load(std::memory_order_relaxed);
    // std::memory_order_relaxed : atomic만 보장, re-ordering은 보장하지 않음, 단, 오버헤드가 가장 작다
    x.store(n1, std::memory_order_relaxed);
}

void goo()
{
    int n2 = x.load(std::memory_order_relaxed);
    y.store(100, std::memory_order_relaxed);
}

int main()
{
    std::thread t1(foo);
    std::thread t2(goo);
    t1.join(); t2.join();
}
#include <thread>
#include <atomic>
#include <cassert>

std::atomic<int> data1 = 0;
std::atomic<int> data2 = 0;
std::atomic<int> flag = 0;

void foo()
{
    data1.store(100, std::memory_order_relaxed);
    data2.store(200, std::memory_order_relaxed);
    flag.store(1, std::memory_order_relaxed);
}

void goo()
{
    if(flag.load(std::memory_order_relaxed) > 0)
    {
        // re-ordering이 보장되지 않기에 assert이 발생할 수 있다.
        assert(data1.load(std::memory_order_relaxed) == 100);
        assert(data2.load(std::memory_order_relaxed) == 200);
    }
}

int main()
{
    std::thread t1(foo);
    std::thread t2(goo);
    t1.join(); t2.join();
}
void foo()
{
    data1.store(100, std::memory_order_relaxed);
    data2.store(200, std::memory_order_relaxed);
    flag.store(1, std::memory_order_release);
    // std::memory_order_release 이전의 코드는 std::memory_order_acquire이후에 읽을수 있음을 보장
    // 무조건 data1.store(100, std::memory_order_relaxed);, data2.store(200, std::memory_order_relaxed); 실행을 보장
}

void goo()
{
    if(flag.load(std::memory_order_acquire) > 0)
    {
        // re-ordering이 보장되지 않기에 assert이 발생할 수 있다.
        assert(data1.load(std::memory_order_relaxed) == 100);
        assert(data2.load(std::memory_order_relaxed) == 200);
    }
}
#include <thread>
#include <atomic>
#include <cassert>

std::atomic<int> data1 = 0;
std::atomic<int> data2 = 0;

int main()
{
    // std::memory_order_seq_cst : atomic, re-ordering 모두 보장해 달라
    data1.store(100, std::memory_order_seq_cst);
    data2.store(200, std::memory_order_seq_cst);
    data2.store(300);   // 디폴트가 std::memory_order_seq_cst이고 오버헤드가 가장 큼.
}

About Taehyung Kim

안녕하세요? 8년차 현업 C++ 개발자 김태형이라고 합니다. 😁 C/C++을 사랑하며 다양한 사람과의 협업을 즐깁니다. ☕ 꾸준한 자기개발을 미덕이라 생각하며 노력중이며, 제가 얻은 지식을 홈페이지에 정리 중입니다. 좀 더 상세한 제 이력서 혹은 Private 프로젝트 접근 권한을 원하신다면 메일주세요. 😎

Star
Useful Links