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의 경우 꽤 의미있는 차이가 있다고 하는데… 안써봐서 잘…
- 1)
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();
}