깃허브 주소 : CPP20_GameServer
이번에는 Reader-Writer Lock 모듈을 만들어볼 차례이다.
이 모듈은 원자적 CAS에 기반한 스핀락 방식으로 구현할 생각이다.
일반 mutex로도 쉽게 만들 수 있지만, 짧은 보유 시간 구간에서 스핀락의 장점을 보여줄 예정이다. 물론 보유 시간이 길어질수록 스핀은 낭비가 커지므로 상황에 따라 mutex가 더 유리할 수 있기에, 용도에 맞게 유연하게 선택하는 태도가 중요하다.
구현에 앞서 락의 상태를 표현하기 위한 enum을 정의하고 각 비트의 의미를 부여했다.
Lock.ixx 파일
ACQUIRE_TIMEOUT_TICK은 일정 시간 동안 락을 획득하지 못했을 때 진단을 위한 한계치로 쓰이며, 디버그 환경에서 크래시를 유발하거나 로깅을 트리거하는 용도로 사용하면 좋다.핵심 상태 변수로는 _lockFlag와 _writeCount를 사용하고, Write와 Read 계열 함수는 이들을 통해 락의 소유와 공유 상태를 판단한다.
이번에도 C++20을 사용해서 atomic 정수형 원자에 memory_order_acquire/release/acq_rel을 명시해 일관된 메모리 가시성을 확보할 것이다.
WriteLock은 어떤 스레드도 소유하지도 공유하지도 않는 상태에서 경합하여 소유권을 얻는 함수이다.
WriteLock(1)
WriteLock(2)
우선 _lockFlag의 상위 비트를 확인해 현재 스레드가 이미 writer인지 검사한다.
재진입 가능한 설계라면 동일 스레드가 다시 WriteLock을 요청했을 때 _writeCount만 증가시키고 즉시 반환하도록 처리한다.
새로 획득해야 하는 경우에는 데드라인을 계산해 과도한 스핀이 길어지지 않도록 한다.
그리고 기대값 expected를 읽고, 현재 상태가 “빈 상태”인지 점검한다.
들어갈 수 있다면 상위 비트에 “현재 스레드 ID를 인코딩한 값”을 얹은 desired를 만들고 CAS를 시도한다.
스핀락을 돌다가 안되면 스레드를 wait로 잠깐 재우는 방식을 채택했다.
이렇게 기다리는 스레드는 나중에 나올 WriteUnlock 함수나 ReadUnlock 함수를 통해서 스레드를 깨우면 된다.
WriteUnlock
그래서 _lockFlag의 값을 가져왔을 때 Read한 흔적이 없어야되고 lockCount가 0이라면 Write할 수 있는 환경을 만들어주기 위해서 _lockFlag에 Empty값을 넣어준다.
그리고 아까 재웠던 스레드를 깨워주기 위해서 notify_all을 사용하면 된다.
ReadLock은 아무도 소유하고 있지 않을 때 공유 카운트를 높이는 함수이다.
ReadLock(1)
ReadLock(2)
나머지 코드는 WriteLock하고 비슷하지만 다른 부분을 한번 보자.
일반 경로에서는 _lockFlag를 읽어 상위 비트에 writer가 있는지 조사한다.
writer가 있으면 진입을 금지하고 적절히 대기하며, writer가 없으면 하위 비트에서 reader 수를 꺼내 expected를 만들고, expected + 1을 desired로 CAS를 시도한다.
나머지는 WriteLock과 같기 때문에 생략하겠다.
ReadUnlock
ReadUnlock은 하위 비트의 reader 수를 1 감소시키고, 마지막 reader가 나갈 때 대기 중인 스레드를 깨운다.
여기서 깨알로 C++20에 나오는 unlikely는 저 코드는 흔하게 나오는 코드가 아니라는 힌트를 던져줘서 최적화에 도움을 주는 코드이다.
RAII를 통해 락의 생애를 자동으로 관리하는 LockGuard는 필수이다.
LockGuard
Lock 모듈 인터페이스 파일에 구현한 코드이다.Read용과 Write용 가드를 분리해 생성자에서 Lock()을, 소멸자에서 Unlock()을 호출하게 하면 예외 경로에서도 안정적으로 락이 해제된다.
이 객체를 사용해서 우리가 Reader-Writer 기능을 사용하고 싶을때 간단하게 매크로를 선언하면 사용할 수 있게 만들 것이다.
사실 이부분에서 우리가 C++20의 기능을 사용하면서 모듈에 최적화하게 만들고 싶었지만 아직 그런 기능을 제공해주지 않기도 하고, 아직 매크로가 현업에서도 사용되기 때문에 이번에도 매크로 기능을 사용할 예정이다.
CoreMacro.h 파일
이렇게 매크로를 선언해서 사용자가 Reader-Writer를 사용하고싶다면 USE_LOCK 매크로만 선언하면 된다.
GameServer 구현파일
이렇게 GameServer에서 TestLock을 만들어서 사용 예시처럼 사용하면 된다.
반드시 USE_LOCK을 먼저 선언하고, Read하고 싶은 부분에는 READ_LOCK을 쓰면 되고, Write하고 싶은 부분에는 WRITE_LOCK을 사용하면 된다.
GameServer 구현파일
이렇게 테스트 코드를 만들고 사용하면 다음과 같은 결과가 나온다.
실행 결과
아주 잘 작동하는 걸 볼 수 있다.
이제 이 기능을 사용해서 Lock 관련 일을 처리하면 된다.
Inflearn [Rookiss][C++과 언리얼로 만드는 MMORPG 게임 개발 시리즈] Part4: 게임 서버