목록Game Dev (261)
KoreanFoodie's Study
[C++ 게임 서버] 1-14. 메모리 모델 (atomic, 코드 재배치) 핵심 : 1. atomic 연산이란, 말 그대로 해당 연산이 '원자적'으로 수행된다는 뜻이다. '원자적' 이라는 것은, 해당 연산 사이에 다른 연산이 끼어들지 않는다는 것을 의미한다. 2. 컴파일러는 여러 이유(주로 최적화)로 코드를 재배치하는데, 이러한 memory order 는 모든 쓰레드들이 '수정 순서' 에 동의한다는 원칙을 지켜야 한다. 3. 대표적으로 다음 세 가지의 메모리 모델 옵션이 있다 : memory_order_relaxed, memory_order_release, memory_order_seq_cst 원자적 연산이란 무엇일까? 멀티쓰레드 환경에서 원자적 연산을 마치 일시적으로 락을 건다고 생각하기 쉽지만, 정확..
[C++ 게임 서버] 1-13. CPU 파이프라인 (feat. 코드 재배치) 핵심 : 1. 컴파일러는 어셈블리어를 배치하는 과정에서 코드 재배치를 할 수 있다. 이 과정에서 의도치 않은 결과가 도출될 수 있다. 2. CPU 파이프라인은 CPU 가 명령어를 효율적으로 하기 위한 시스템이다. CPU 파이프라인에 대해 공부를 하게 되면, 최적화를 위해 컴파일러가 임의로 어셈블리 코드를 재배치한다는 것을 알 수 있다. 싱글 쓰레드에서는 순차적으로 로직이 실행됨이 보장되지만, 멀티 쓰레드에서는 꼭 의도대로 프로그램이 동작하지 않을 수 있는데... 다음 코드를 보자. volatile bool ready = false; // 가시성, 코드 재배치 int32 x = 0; int32 y = 0; int32 r1 = 0;..
[C++ 게임 서버] 1-12. 캐시 (Temporal Locality, Spatial Locality) 핵심 : 1. RAM 으로부터 데이터를 가져올 때, 자주 쓰일 것으로 예상되는 데이터는 캐시에 저장된다 2. 캐싱 철학에는 Temporal Locality 와 Spatial Locality 가 있는데, 각각 '최근에 사용된 녀석은 다시 사용될 확률이 높다' , '메모리상으로 가까이 있는 녀석은 함께 사용될 확률이 높다' 라는 추론을 기반으로 한다. 3. 실제 메모리 조회를 할 때는 위의 Temporal Locality 와 Spatial Locality 를 고려하여 설계를 하는 것이 좋다. 흔히 우리는 캐시를 통해 값을 조회할 때, 우리가 필요한 정보만 얻어올 것이라고 기대하지만, 실제로는 그렇지 않다..
[C++ 게임 서버] 1-11. future, promise, packaged_task 핵심 : 1. future, promise, packaged_task 는 단발성 이벤트를 비동기적으로 실행할 때 유용하다. 2. 세 녀석의 차이는 Level of Control 에 달려 있다. future 는 특정 함수의 작업을 맡기고, packaged_task 는 작업을 만든 다음, 쓰레드에 이를 실어 보낸다. 마지막으로, promise 는 작업을 실어 보내면서, 동시에 실어 보내는 함수 내부에서 future 에 넘길 값을 커스텀하게 세팅할 수 있다. 3. '비동기적'인 작업과 '멀티쓰레드' 는 다른 의미이다. 어떤 작업이 비동기적으로 실행된다고 해서, 꼭 멀티쓰레드를 이용할 필요는 없다. 사실 자주 쓰이는 녀석들은..
[C++ 게임 서버] 1-10. Condition Variable (조건 변수) 핵심 : 1. 조건 변수(Condition Variable) 은 유저 레벨 오브젝트로, 동일한 프로그램 내에서 접근 가능하다. 2. 조건 변수를 이용하면 하나의 쓰레드 혹은 모든 쓰레드에 시그널을 전달할 수 있다. 3. 조건 변수는 Lock 과 페어를 이루어 사용한다! 저번에 Handle 을 이용해 이벤트를 구현했던 것을 조금 발전시켜, 이번에는 condition variable 을 사용해볼 것이다. 사실 condition variable 은 Handle 을 사용하는 것보다 조금 더 우아한(?) 방식이며, 조건을 체크하여 락을 잡고 푸는 기능을 제공해 준다. 또한 condition variable 은 유저 레벨 오브젝트이므로..
[C++ 게임 서버] 1-9. 이벤트 구현 (+ Handle) 핵심 : 1. 이벤트를 사용하면 SpinLock 에 비해 CPU 낭비를 줄일 수 있는 장점이 있다. 2. 이벤트의 개념은 간단하다. 핸들을 만든 후, Consumer 는 핸들을 참고하여 깨울 때까지 준비 상태에 들어가고, Producer 가 준비가 되었을 때 핸들에게 이벤트를 보내기만 하면 된다. 3. 핸들은 커널 오브젝트에 해당하여, 여러 쓰레드가 이를 활용할 수 있다. 이제 Lock 구현을 함에 있어 '무작정 기다리기' 와 '잠깐 기다리기' 를 구현해 보았으니, 마지막 순서가 남아있다. 바로... '제 3자가 깨워주기' 인데, 여기서 '제 3자'라 함은 일반적으로 OS 의 스케줄러를 의미하고, '깨워준다' 는 것은 해당 쓰레드에 시그널을 ..
[C++ 게임 서버] 1-8. Sleep 개념과 구현 핵심 : 1. Lock 을 잡을 수 없을 때, 일정 시간 동안 기다리게 할 때 sleep 을 사용한다. 2. Sleep 을 사용하면 일정 시간 동안 해당 쓰레드는 준비 상태에 있다가 다시 돌아온다. 이 시간동안에는 CPU 자원을 사용하지 않는다. 대신 Kernel Space 로 전환되는 과정에서의 비용은 발생한다(컨텍스트 스위칭). 3. yield 는 쓰레드에게 주어진 Time Slice 를 즉시 반납하도록 하며, 스케줄러에게 쓰레드가 어떤 일을 할지 알아서 정해달라고 요청을 하게 된다. 이전에 Lock 구현 이론에서 설명한 것 중 두번째인, '랜덤 시간 후 복귀' 메타가 바로 sleep 이다. 이전 글에서 SpinLock 을 이용해 '무작정 기다리는..
[C++ 게임 서버] 1-7. SpinLock 핵심 : 1. SpinLock 은 Lock 을 잡기 위해 '무작정 기다리는' 방식이다. 2. SpinLock 은 atomic 변수와 lock, unlock 함수로 쉽게 만들 수 있다. compare_exchange_strong 함수를 기억하자. 3. SpinLock 은 불필요한 CPU clock 을 낭비하지만, 때로는 컨텍스트 스위칭에 드는 것보다 비용이 저렴할 수도 있다. 이제 본격적으로 SpinLock 을 구현해 보자. SpinLock 은 간단하다. 그냥 Lock 을 잡을 때까지 무작정 While 문을 돌면서 기다리도록 만들 것이다 🤣 간단하게 생각하면 아래와 같이 구현하겠지만... int sum = 0; class SpinLock { private: b..
[C++ 게임 서버] 1-6. Lock 구현 이론 핵심 : 1. Lock 을 구현하는 방식은 크게 세 가지로 나뉜다. 무작정 기다리기, 잠시 후 돌아오기, 깨워줄 때까지 기다리기 2. SpinLock 을 사용하여 무작정 기다리거나, 어느 정도 Sleep 을 하고 난 후 Lock 을 다시 잡으려고 시도할 수도 있다. 3. 혹은 다른 일을 하다가, Lock 을 잡을 수 있게 되는 상태가 되었을 때 노티를 달라고 요청할 수도 있다. 예를 들어 Condition Variable 이 있다. 이 과정에서 컨텍스트 스위칭이 일어날 수 있다! 본격적으로 Lock 을 구현하기 전, Lock 을 구현하는 일반적인 방식 3 가지에 대해 먼저 소개하고 가도록 하겠다. 사실 내용은 간단하다. Lock 이 이미 걸린 상태라고 할..
[C++ 게임 서버] 1-5. DeadLock 의 개념과 기초 (+ std::lock) 핵심 : 1. DeadLock 상황은, 여러 쓰레드들이 Lock 을 서로 잡고 있어 풀거나 새로 잡지 못하는 교착 상태를 의미한다. 2. DeadLock 상황은 Lock 을 잡는 순서를 잘 조절하거나, 적절한 Lock 을 사용하여 회피할 수 있다. 3. std::lock 은 여러 개의 Lock 을 DeadLock 없이 한꺼번에 잡을 수 있도록 도와준다. 다음과 같이, Lock 이 2개 존재하고 연산을 위해 Lock 2 개를 전부 잡아야 한다고 가정하자. int sum = 0; mutex mutexAdd; mutex mutexSub; void Add() { for (int i = 0; i < 1000; ++i) { mu..