c+11에서 추가된 것 중 가장 중요한 것은 memory model이다.
메모리 절대 법칙
atomic 연산에 한해, 모든 쓰레드가 동일 객체에 대해서 동일한 수정 순서를 관찰한다.
동일한 수정 순서를 관찰한다의 의미는?
시간의 흐름을 거슬러서 관찰할 수 없다는 뜻. 가시성은 해결되지 않는다.
예를 들어, 0을 관찰하고 2를 관찰했다면, 다른쓰레드 또는 같은 쓰레드가 다시 관찰할 때 절대 0을 관찰 할 수 없다.
그러나 만약 2 관찰 이후 시간이 흘러도 1이나 5가 관찰되지 않을 수는 있다. 또한 0,2,1,5 모두 거쳐 관찰되지 않을 수 있다. 예를 들어, 0 관찰이후 다음에 5가 관찰될 수 있다.
원자적 연산이란?
CPU가 한 번에 연산한다 -> 원자적 연산
int64 num;
void Thread_1()
{
num = 1;
}
void Thread_2()
{
num = 2;
}
CPU가 64비트 연산을 지원하면 num = 1의 연산이 원자적으로 이뤄질 것이다. 32비트 연산을 지원하면 32비트, 32비트 2번에 나눠서 연산이 이뤄질 것이다. 이는 원자적 연산이 아니다.
다음과 같은 코드로 객체에 대해 원자적연산이 지원되는지 확인할 수 있다.
atomic<bool> flag;
int main()
{
flag = false;
// flag.is_lock_free();
flag.store(true, memory_order::memory_order_seq_cst);
bool val = flag.load(memory_order::memory_order_seq_cst);
// 이전 flag 값을 prev에 넣고 flag값을 수정
{
//bool prev = flag;
//flag = true;
bool prev = flag.exchange(true);
}
// CAS(Compare-And-Swap) 조건부 수정
{
bool expected = false;
bool desired = true;
flag.compare_exchange_strong(expected, desired);
// 의사코드
//if (flag == expected)
//{
// flag = desired;
// return true;
//}
//else
//{
// expected = flag;
// return false;
//}
}
{
bool expected = false;
bool desired = true;
flag.compare_exchange_weak(expected, desired);
// 의사코드
//if (flag == expected)
//{
// 다른 쓰레드의 Interruption 받아서 중간에 실패할 수 있음.
// if(묘한상황)
// return false;
// flag = desired;
// return true;
//}
//else
//{
// expected = flag;
// return false;
//}
}
}
메모리 모델 활용 예
atomic<bool> ready;
int32 value;
void Producer()
{
value = 10;
ready.store(true, memory_order::memory_order_release);
// ------------------- 절취선 ------------------------
}
void Consumer()
{
// ------------------- 절취선 ------------------------
while (ready.load(memory_order::memory_order_acquire) == false)
;
cout << value << endl;
}
int main()
{
ready = false;
value = 0;
thread t1(Producer);
thread t2(Consumer);
t1.join();
t2.join();
// Memory Mode(정책)
// 1) Sequentially Consistent (seq_cst)
// 2) Acquire-Release (acquire, release)
// 3) Relaxed (relaxed)
// 1) seq_cst (가장 엄격 = 컴파일러 최적화 여지 적음 = 직관적)
// 가시성 문제 바로 해결! 코드 재배치 바로 해결!
// 2) acquire-release
// 딱 중간!
// release 명령 이전의 메모리 명령들이, 해당 명령 이후로 재배치 되는 것 금지
// 그리고 acquire로 같은 변수를 읽는 쓰레드가 있다면
// release 이전의 명령들이 -> acquire하는 순간에 관찰 가능(가시성 보장)
// 3) relaxed (자유롭다 = 컴파일러 최적화 여지 많음 = 직관적이지 않음)
// 너무나도 자유롭다!
// 코드 재배치도 멋대로 가능! 가시성 해결 NO!
// 가장 기본 조건 (동일 객체에 대한 동일 관전 순서만 보장)
// 인텔, AMD의 경우 애당초 순차적 일관성을 보장을 해서,
// seq_cst를 써도 별다른 부하가 없음.
// ARM의 경우 꽤 차이가 있다고 함.
}
가장 엄격한 규칙인 seq_cst를 사용해도 인텔, AMD CPU에서 부하가 크지 않기 때문에 보통 이것을 사용한다.
참고로 코드 재배치를 막기위한 fence를(절취선) 긋고 싶을 때 atomic을 사용해야만 하는 것은 아니다.
void Producer()
{
value = 10;
// 참고 : 절취선을 긋고 싶을 때 atomic을 써야만 하는 건 아님
std::atomic_thread_fence(memory_order::memory_order_release);
// ------------------- 절취선 ------------------------
}
void Consumer()
{
// 참고 : 절취선을 긋고 싶을 때 atomic을 써야만 하는 건 아님
std::atomic_thread_fence(memory_order::memory_order_acquire);
// ------------------- 절취선 ------------------------
cout << value << endl;
}
참조 : [C++과 언리얼로 만드는 MMORPG 게임 개발 시리즈] Part4: 게임 서버 - 인프런 | 강의 (inflearn.com)
Lock-based stack, queue (0) | 2021.08.11 |
---|---|
Thread local storage (0) | 2021.08.11 |
CPU 파이프라인 (0) | 2021.08.09 |
캐시 (0) | 2021.08.07 |
Future (0) | 2021.08.07 |
댓글 영역