강의

멘토링

로드맵

Inflearn brand logo image

인프런 커뮤니티 질문&답변

timesilvergold님의 프로필 이미지
timesilvergold

작성한 질문수

[C++과 언리얼로 만드는 MMORPG 게임 개발 시리즈] Part4: 게임 서버

Reader-Writer Lock

ReaderWriterLock 부분 질문입니다.

작성

·

91

0

void Lock::WriteLock()

부분에서

 

제가 실수로 expected = EMPTY_FLAG;이 부분을 while(true) 밖에 선언했습니다.

CAS과정에서 expected가 갱신되는 것을 깜빡하구요.

그럤더니, 당연히 크래시가 났는데 저는 expected가 다른 ReadLock이나 WrtieLock으로 잘못된 값으로 갱신되면 EMPTY_FLAG랑 비교하는 CAS과정을 통과할 수 없게 되어 yield를 반복하다가 TIME_OUT 크래시가 날줄 알았습니다. 그런데 ReadUnlock쪽에서 Multiple Unlock 크래시가 뜨더군요.

행여나 쌍을 맞춰주지 않았을까, 코드까지 다운로드해서 비교헀더니 딱 저 부분만 수정하니 잘 동작해서

왜 Multiple Unlock이 나타나는지 의문입니다. 아무래도 RAII 객체의 LockGuard부분에서 소멸자를 호출하는과정에서 Multiple_Unlock이 뜨는 것 같은데, TIMEOUT -> 스택 풀기 과정에서 (소멸자 호출)로 Multiple_Unlock이 뜨기에는 10초의 시간이 있음에도 실행하자마자 바로 크래시가 나서, 어느 부분에서 ReadUnlock까지 가는지를 모르겠습니다.

 

아무리 생각해도 READ_LOCK에서 lock을 잡지 못하면, Unlock까지 도달할 일이 없지 않나요?

디버깅을 찍어봐도 어디서 Unlock에 도달하지는지 모르겠습니다

 

디버깅 정보 올립니다.

스크린샷 2025-05-10 231129.png.webp

 

혹시 이럴때 CRASH를 낸 주체를 정확히 모르겠으면 어떤식으로 디버깅 하면서 풀어나가면 좋을까요?

ChatGPT는 어디가 어떻게 에러를 내는지 명쾌한 해답을 못주네요

답변 2

1

Rookiss님의 프로필 이미지
Rookiss
지식공유자

우선 100% 재현되는 문제는 질문을 하면 안 되고,
괴롭더라도 스스로 고민해보면서 알아내셔야 합니다.
서버 쪽은 어차피 이런 불가사의한 일들의 연속입니다.
심지어 재현 안 되는데 원인을 찾아야 하는 경우도 많은데
그 때마다 공식이 있진 않고 그냥 근성과 코드 분석으로 극복할 수밖에 없습니다.

expected = EMPTY_FLAG;
~의 위치 문제라면 그 부분 때문에 단순히 로직이 꼬인 것이 확률이 높고,
로직이 꼬인 상태에선 어떤 상황이라도 발생할 수 있으니
꼭 Lock/Unlock 짝의 문제라고 장담할 수 없습니다.
아무튼 일주일 후에도 돌아왔는데 해결을 못하셨으면 그 때 저도 살펴보겠습니다.

ㅎㅎㅎ 화이팅!

 

감사합니다!

대략적인 문제는 찾은 것 같습니다.

디버깅을 찍어보니 ReadUnlock 부분에서 _lockFlag의 값이 2가지 형태로 관측되었습니다.

 

  1. 0x000(N)'FFFF

     

  2. 0xFFFF'FFFF (언더 오버플로)

우선 이 값이 등장하게 된 배경은 다음과 같습니다.

 

A 스레드가 ReadLock을 획득해 _lockFlag을 0x0000'000A으로 변경했습니다.

이 상태에서 B 스레드가 WriteLock을 획득하려고 CAS 연산을 시도합니다.

CAS과정에서 expected = EMPTY_FLAG; 값이 while 밖에 있으므로

expected값이 ReadLock을 획득한 _lockFlag값으로 변경됩니다. 이상태에서

"우연히" 다음 CAS시도까지 ReadUnlock이 되지 않았고, CAS 연산이 성공하게 됩니다.

바로 _lockFlag가 0x000B'0000으로 덮어씌워지고 이상태에서

 

ReadUnlock이 먼저 실행되면 0x000B'0000 - 1인 0x000A'FFFF가 되고

\lockFlag의 이전의 READ_COUNT_MASK는 0이므로

MULTIPLE_UNLOCK 크래시가 나게 됩니다.

 

WriteUnlock이 먼저 실행되면 0x00000000인 EMPTY_FLAG로 _lockFlag가 밀리고

이상태에서 ReadUnlock이 실행되면 2^32 - 1 값인 uint32 언더플로우를 발생시켜

0xFFFF'FFFF 값을 볼 수 있고 마찬가지로 이전 값의 READ_COUNT_MASK가 0이여서 똑같은 크래시가 발생하게 됩니다.

 

저는 운좋게 이 두개의 값을 관측할 수 있었고 이를 역추적하면서 문제를 알아낼 수 있었습니다. 이 과정에서 각 스레드의 호출이 어떻게 돌아가는지 디버깅을 하지 못했는데

각 스레드들을 시간순서대로 관측할 수 있는 방법이 있을까요? 중단점을 걸고 진행해도 주 스레드에서 밖에 이를 추적할 수 없어 답답한 느낌이 듭니다.

정상적인 코드라면 ReadLock을 잡은 상태에서 WriteLock을 잡을 수 없습니다.

CAS 성공/실패와 관계없이 매번 expected가 EMPTY_FLAG로 초기화되기 때문이죠

그래서 잘 동작합니다.

Rookiss님의 프로필 이미지
Rookiss
지식공유자

멀티쓰레드 환경에서는 BreakPoint를 잡고 분석하는 것이 한계가 있습니다.
말씀대로 언제든지 다른 쓰레드가 개입해서 수정하기 때문이죠.
뾰족한 방법이 있다고 보긴 힘들고, 다른 쓰레드의 개입까지 고려하면서 코드를 읽는
상상의 나래를 펼치면서 해결하는 수밖에 없습니다.

0

안녕하세요, 인프런 AI 인턴입니다. timesilvergold님.

ReaderWriterLock 관련 문제에서 생긴 크래시와 문제 해결 방법에 대해 답변드리겠습니다.

질문을 정리해보면, WriteLock() 함수에서 expected = EMPTY_FLAG;를 반복문 밖에 두셨기 때문에 CAS(Compare-And-Swap) 과정에서 기대했던 EMPTY_FLAG와의 비교가 반복적으로 실패했습니다. 그리고 이것이 Multiple Unlock 크래시로 이어졌다는 점인데, 이 상황을 조금 더 설명드리겠습니다.

  1. CAS 실패와 무한 루프:

    • WriteLock()에서 expected 변수가 제대로 초기화되지 않으면 CAS 비교가 지속적으로 실패하여 루프를 벗어나지 못할 수 있습니다. 이 경우 lock을 잡지 못하고 반복적으로 waiting 상태에 들어가게 되어 성능 이슈가 발생할 수 있습니다.
  2. Unexpected Unlock:

    • 논리의 흐름상 WriteLock(), ReadLock()이 두 번 이상 호출되었는데, Unlock이 정확히 그와 맞지 않으면 Multiple Unlock 크래시가 발생할 수 있습니다. 이는 특정 시나리오에서 lock이 잡힌 상태인데 unlock이 실패한 상태로 인해 발생할 수 있으며, 특히 데드락 상황에서는 더 복잡한 크래시로 진화할 수 있습니다.
  3. 디버깅 방법:

    • 스택 트레이스 확인: 캡처된 이미지처럼, IDE에서 콜 스택을 확인하세요. 어디서 Unlock이 호출되었는지를 추적하여 실제 문제가 발생한 지점을 찾을 수 있습니다.
    • 로그 추가: lock과 unlock 전후로 로그를 추가해서 상태를 확인하는 방법도 있습니다. 멀티스레드 환경에서는 직접적인 디버깅이 힘들 수 있어 로그를 통해 순서를 확인하는 것이 유효합니다.
    • RAII 객체 확인: 소멸자에서 unlock이 호출될 가능성이 크므로, RAII 객체의 수명과 호출 흐름을 분석해보세요.

관련 질문을 통해 동일한 주제에 대한 논의를 보실 수 있습니다:

위 링크는 유사한 문제해결 사례를 제공하며, 질문 내용의 맥락을 이해하는 데 도움을 드릴 수 있을 것입니다.

저는 질문자님의 이해를 돕고자 지식공유자님의 기존 답변을 참고하여 유사한 답변 링크를 추천해드리고 있습니다.
현재 베타 기능으로 답변이 만족스럽지 않을 수 있는 점 양해 부탁드립니다. 🙏 추가적으로 궁금한 점이 있으시면, 이어서 질문해 주세요. 곧 지식공유자께서 답변해 주실 것입니다.

timesilvergold님의 프로필 이미지
timesilvergold

작성한 질문수

질문하기