※ Rookiss님의 [C++과 언리얼로 만드는 MMORPG 게임 개발 시리즈] Part4: 게임 서버 강의를 보고 정리한 글입니다.
멀티 쓰레드 환경에선 하나의 값에 동시에 참조하는 경우(write하는경우) 경합조건이일어난다.
이를 방지하기위해 2가지 방법을 사용해왔었다.
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 /*기본 모델*/);
크게 나눠보면 이렇게 나뉜다.
1) Sequentially Consistent (seq_cst)
2) Acquire-Release (consme, acquire, release, acq_rel)
3) Relaxed (relaxed)
하지만 비슷한 것들을 모두 제거하면 이정도로 남는다.
이 정책들은 기본적으로 모두 보장하는 것이 있다.
“atomic 연산에 한해, 모든 쓰레드가 동일 객체에 대해서 동일한 수정 순서를 관찰한다”
무슨 소리인가 싶겠지만
num이라는 수가 1, 2, 4, 3 순으로 수정을 거쳤다고 할 때, 2로 갱신이 되었다면 다음 갱신 때는 1로 갱신될 수 없다.
갱신 데이터가 1 → 4 → 3, 1 → 3, 이런 식으로 될 순 있지만 역순으로 될 순 없다는 이야기다.
이제 정책을 세세히 알아보자
seq_cst는 가장 엄격해 가시성 문제와 코드 재배치 문제가 바로 해결된다.
인텔 , AMD의 경우 애당초 순차적 일관성을 보장해서, seq_cst 버전을 써도 별 부하가 없다!
⇒ 다른 것도 쓸 수 있지만 애당초 이것만 써도된다! 하지만 다른 것들도 알아서 안좋을 건 없으니 공부해보자
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 하는 순간에 최신으로 갱신된다!
코드재배치 멋대로 가능, 가시성 해결 NO
가장 기본 조건(동일객체에 대한 동일 관전 순서만 보장)만 보장한다.
이외에 atomic을 사용안하고도 재배치되는 것을 금지하기위핸 이걸 쓰면된다.
atomic_thread_fence(모델):
void Producer() {
// -------------------------------- 위 코드가 아래로 못넘어감-
std::atomic_thread_fence(memory_order_seq_cst);
}