(C++ : IOCP-6) Memory model

Posted by : at

Category : Cpp   iocp



C++11에서 추가된 가장중요한 것은 –> Memory Model이라 할 수 있다

일단 앞 내용을 간단히 복습하고 들어가자

  • 여러 쓰레드가 동일한 메모리에 동시 접근(Write를 할경우 문제)
  • Race Condition 조건 경합이 일어난다
  • 따라서 Undefined Behavior 정의되지 않은 행동이 발생하며
    • Lock, Atomic을 이용해 해결을 해야한다.
  • atomic 연산에 한해, 모든 쓰레드가 동일 객체에 대해서 동일한 수정 순서를 보인다.
  • 뭔소린지 모르겠으니 아래 Example을 참조
// atomic 선언
atomic<int64> num;

vod Thread_1()
{
    num.store(1);   // num = 1
}

vod Thread_2()
{
    num.store(2);   // num = 2
}

// ... 여러개의 Thread가 있다고 가정하자


void Thread_Observer()
{
    while(true)
    {
        int64 value = num.load();
        // 여기 사이에서 변할수 있지만... 이거는 일단 무시
        std::cout << value << std::endl;
        // 관찰 결과는?
    }
}
# 시간에 따라 atomic<int64> num이 아래와 같이 변경된다고 생각해보자.

-----(0)---(5)----(4)----(3)-------------<시간>
   ^            ^
   |            |
 (관찰1)      (관찰2)

  • atomic 연산에 한해, 모든 쓰레드가 동일 객체에 대해서 동일한 수정 순서를 보인다.
  • 위말은 기존의 atomic연산이 아닐경우, 관찰 시점에 관계없이 과거의 데이터를 관찰할 수 있다.
  • 예를 들어 (관찰2)에서 관찰했는데 0이 나올수 있음(왜인지는 이전강의 참조)
  • 단, atomic에 한에서는 관찰시점의 미래 데이터는 당연히 모르겠지만 현재 데이터를 보여준다는 말

그럼 atomic하게 CPU가 처리하고있는지 아닌지 어떻게 알지? 그냥 atomic만써주면 다 원자적으로 처리하는가?
당연히 아니다. 예를들어 32bits환경에선 64bits 변수를 atomic하게 데이터변경이 불가능(메모리주소를 2번 옮겨야함)
atomic하게 처리가능한지 확인할 방법(is_lock_free)이 있다.

atomic<int64> v;
cout << v.is_lock_free() << endl; 
// 1 - CPU 자체적 atomic 가능

struct Knight
{
    int32 level;
    int32 hp;
    int32 mp;
};

atomic<Knight> v;
cout << v.is_lock_free() << endl;   
// 0 - CPU 자체적 atomic 불가능
// 내부에서 lock을 하든 어떠한 방식으로 atomic하게 처리해줌.

// atomic 선언
atomic<int64> num;

vod Thread_1()
{
    // num = 1
    // num.store(1);
    // 아래와 동일한 의미이다. (메모리 정책을 memory_order::memory_order_seq_cst로 써달라)
    num.store(1, memory_order::memory_order_seq_cst);
}

vod Thread_2()
{
    // num = 2
    // num.store(2);
    num.store(2, memory_order::memory_order_seq_cst);
}

// ...


void Thread_Observer()
{
    while(true)
    {
        // int64 value = num.load();
        int64 value = num.load(memory_order::memory_order_seq_cst);
        // 관찰 결과는?
    }
}

그럼 memory_order::memory_order_seq_cst이란 뭘까?


잠깐 복습으로 atomic에 값을 읽고, 쓰는법을 정리해보자면

atomic<bool> flag;

int main()
{
    {
        // 우선 atomic에 값을 읽고, 쓰는 법을 간단히 정리해보자면
        flag.store(true, memory_order::memory_order_seq_cst);

        bool val = flag.load(memory_order::memory_order_seq_cst);
    }

    {
        /*
        atomic의 현재값을 저장해 두고 다른값을 대입하고자 한다면
        */

        // 이전 flag 값을 prev에 넣고, flag 값을 수정
        bool prev = flag;
        flag = true;
        // 이 과정이 위 코드와 같이 두 줄로 이루어질경우 다른 Thread에서 flag값을 변경해버릴 위험이 존재

        // 이렇게 처리가능
        bool prev = flag.exchange(true);
        // prev에 현재값 넣고 exchange 매개변수의 값을 flag에 넣어달라
    }
    
    {
        // CAS(Compare-And_Swap) 조건부 수정
        bool expected = false;
        bool desire = true;

        flag.compare_exchange_strong(expected, desire);
        /*
        * 예전에 정리했지만 다시 내부 의사코드를 적어보자면
        * 이걸 atomic하게 처리해 준다는 말.
        if(flag == expected)
        {
            expected = flag;
            flag = desired;
            return true;
        }
        else
        {
            expected = flag;
            return false;
        }
        */
    }
}

다시 Memory_model(memory_order::memory_order_seq_cst이놈)에 대해 설명한다.

  • Memory model 정책
    • Sequentialiy Consistent (seq_cst)
    • Acquire-Release (consume, acquire, release, acq_rel)
    • Relaxed (relaxed)
  • 우선 간단히 정리하면
    • 1) seq_cst : 엄격, 직관적, 코드 재배치(최적화)가 적음
    • 2) acquire-release : 중간
      • acquire-relase로 동작하며 store에서 release를 잡아두면
      • acquire 전까지 코드재배치가 일어나는것을 막아준다.
      • acquire 이후 데이터들이 갱신이 되어 최신 데이터 수신가능
    • 3) relaxed : 자유로움, 비직관적, 코드 재배치(최적화)가 있음
      • 가장 기본 조건인 동일 객체에 대한 동일 관전 순서만 보장해준다.
      • 사실상 사용이 잘 안된다.
    • Intel, AMD의 경우 순차적 일관성을 보장해준다.
    • 따라서 seq_cst를 그냥 쓰면된다.
    • 단, ARM의 경우 꽤 의미있는 차이가 있다고 하는데… 안써봐서 잘…
atomic<bool> ready;
int32 value;

void Producer()
{
    value = 10;
    // 1)
    ready.store(true, memory_order::memory_order_seq_cst);

    // 2)
    // ready.store(true, memory_order::memory_order_release);
    // 여기서 부턴 코드재배치 하지마

    // 3)
    // ready.store(true, memory_order::memory_order_relaxed);
    // memory_order_relaxed일 경우 value = 10;가 read.store아래로 배치될 수 있다는 말.
}

void Consumer()
{
    // 1)
    while(read.load(memory_order::memory_order_seq_cst) == false);

    // 2)
    // while(read.load(memory_order::memory_order_acquire) == false);
    // 이제 코드재배치 가능
    // acquire이후에 데이터들이 갱신이 된다.

    // 3)
    // while(read.load(memory_order::memory_order_relaxed) == false);

    cout << value << endl;
}

int main()
{
    ready = false;
    value = 0;

    thread t1(Producer);
    thread t2(Consumer);
    t1.join();
    t2.join();
}

About Taehyung Kim

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

Star
Useful Links