C++ 메모리 모델

정은성·2023년 3월 17일
1
post-thumbnail

※ Rookiss님의 [C++과 언리얼로 만드는 MMORPG 게임 개발 시리즈] Part4: 게임 서버 강의를 보고 정리한 글입니다.

멀티 쓰레드 환경에선 하나의 값에 동시에 참조하는 경우(write하는경우) 경합조건이일어난다.

이를 방지하기위해 2가지 방법을 사용해왔었다.

  1. Lock(mutex)를 이용한 상호 배타적 접근
  2. Atomic(원자적) 연산 이용

Atomic

is_lock_free(): Atomic에서 원자적 계산을 할 때 lock을 사용하는지 알려주는 함수 ( true: 사용 X, false: 사용)

bool같은건 원자적 계산이 가능하지만 클래스, long 타입 같은 크거나, 구조가 복잡한건 원자적으로 불가능 하기에 내부적으로 lock을 사용해 원자적으로 해결한다.

exchange(boolean): 읽기와 쓰기를 원자적으로 하게 해주는 함수

flag값을 가져오고 true로 바꾸는 과정에서 다른 스레드가 flag의 값을 바꾼다면 유효한 값이 되지않기 때문에 동시에 해야한다.

bool prev = flag.exchange(true); // prev에 false가 들어가고 flag값은 true로 바뀐다.

CAS( Compare-And-Swap) 조건부 수정

compare_exchange_strong/weak(expected, desired): expected값이라면 desired값으로 수정하기

strong과 weak가 있다. 이 둘은 CAS 동작 도중 다른 스레드의 방해를 받아 실패하기도하는데 그때 차이가 들어난다.

bool expected = false;
bool desired = true;

flag.compare_exchange_strong(expected, desired); // 도중 실패시 성공할 때 까지 시도
flag.compare_exchange_weak(expected,desired); // 도중 실패시 끝

Atomic 연산을 이용할 때 메모리 모델을 선택할 수 있다.

우리는 선택을 안하고있었지만 기본적으로 memory_order_seq_cst로 되어있었다.

atomic<bool> ready = false;

ready.store(true, memory_order::memory_order_seq_cst /*기본 모델*/);

memory Model (정책)

크게 나눠보면 이렇게 나뉜다.

1) Sequentially Consistent (seq_cst)
2) Acquire-Release (consme, acquire, release, acq_rel)
3) Relaxed (relaxed)

하지만 비슷한 것들을 모두 제거하면 이정도로 남는다.

  1. seq_cst (가장 엄격 = 컴파일러 최적화 여지 적음 = 직관적)
  2. acquire-release (중간단계)
  3. relaxed (자유롭다 = 컴파일러 최적화 여지 많음 = 직관적이지않음)

이 정책들은 기본적으로 모두 보장하는 것이 있다.

“atomic 연산에 한해, 모든 쓰레드가 동일 객체에 대해서 동일한 수정 순서를 관찰한다”

무슨 소리인가 싶겠지만

num이라는 수가 1, 2, 4, 3 순으로 수정을 거쳤다고 할 때, 2로 갱신이 되었다면 다음 갱신 때는 1로 갱신될 수 없다.

갱신 데이터가 1 → 4 → 3, 1 → 3, 이런 식으로 될 순 있지만 역순으로 될 순 없다는 이야기다.

이제 정책을 세세히 알아보자

seq_cst

seq_cst는 가장 엄격해 가시성 문제와 코드 재배치 문제가 바로 해결된다.

인텔 , AMD의 경우 애당초 순차적 일관성을 보장해서, seq_cst 버전을 써도 별 부하가 없다!

⇒ 다른 것도 쓸 수 있지만 애당초 이것만 써도된다! 하지만 다른 것들도 알아서 안좋을 건 없으니 공부해보자

acquire-release

acquire

해당 명령 이후로 재배치 되는 것을 금지함

void Producer() {
value = 10;

ready.store(true, memory_order::memory_order_release);	
// -------------------------------- 위 코드가 아래로 못넘어감-----------
std::atomic_thread_fence(memory_order_seq_cst);
}

release

해당 명령 이후로 재배치 되는 것을 금지함

void Consumer() {

	// 아래 코드들이 위로 못올라감 -------------------------------------
	while (ready.load(memory_order::memory_order_acquire) == false)
		;
	// relaese 이전 변수들이 모두 갱신된다
	cout << value << endl;

}

또한 acquire로 같은 변수를 읽는 쓰레드가 있다면 release 이전 명령들이 -> acquire 하는 순간에 최신으로 갱신된다!

Relaxed

코드재배치 멋대로 가능, 가시성 해결 NO
가장 기본 조건(동일객체에 대한 동일 관전 순서만 보장)만 보장한다.


이외에 atomic을 사용안하고도 재배치되는 것을 금지하기위핸 이걸 쓰면된다.

atomic_thread_fence(모델):

void Producer() {
	// -------------------------------- 위 코드가 아래로 못넘어감- 
	std::atomic_thread_fence(memory_order_seq_cst);
}

0개의 댓글