상세 컨텐츠

본문 제목

메모리 모델

똑똑한 개발/C++ 게임개발

by 성댕쓰 2021. 8. 10. 22:44

본문

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)

'똑똑한 개발 > C++ 게임개발' 카테고리의 다른 글

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

관련글 더보기

댓글 영역