아래는 네가 붙여준 “제미나이 해석본” + 내가 전에 작성한 자료를 합쳐서, 더 정확하고 실전 관점까지 보강한 “완전판 학습 자료(심화 + 함정/레이스까지 포함)”이야.
(특히 이벤트는 카운팅을 못 한다는 점 때문에, 예제 그대로 쓰면 신호 유실(lost wake-up) 같은 함정이 생길 수 있어서 그 부분을 확실히 잡아줄게.)


[C++ 게임 서버] 이벤트(Event)로 동기화하기 — 완전판 학습 자료

  1. 이 문서의 목표

  2. Busy Waiting(바쁜 대기)가 왜 CPU를 낭비하는지 정확히 이해

  1. Windows Event(커널 오브젝트)로 “필요할 때만 깨우는” 동기화 구조를 구현
  1. Auto-reset / Manual-reset 차이를 “코드 관점”으로 명확히 정리
  1. “강의 예제”를 실전형으로 업그레이드하며, 이벤트가 가진 대표 함정(신호 유실)을 방지하는 패턴까지 습득

  1. 배경: 유저 레벨 락 vs 커널 기반 동기화

1-1. Busy Waiting이 생기는 이유

SpinLock이나 단순 폴링(polling)은 “조건이 만족될 때까지 계속 확인”하는 구조라서,

조건이 자주 만족되면: 오히려 빠를 수도 있음(커널 왕복 비용 없음)

조건이 드물게 만족되면: 대부분 시간이 “아무 일도 안 하면서 CPU만 사용”

1-2. 이벤트 방식의 핵심 아이디어(강의 비유 정제)

기존(폴링): “문고리를 계속 돌려보며 확인”

이벤트: “관리자(커널)에게 ‘열리면 깨워줘’ 요청하고 잠듦”

대기 중에는 스레드가 Block 상태로 들어가 CPU를 거의 쓰지 않음

핵심 문장: “제 3자(커널)가 깨워주는 동기화”


  1. 문제 상황: 폴링 기반 Producer-Consumer의 비효율

[Code 1] 이벤트 적용 전 (폴링 → CPU 낭비)

mutex m;
queue q;

void Producer() {
for (int i = 0; i < 10000; ++i) {
{
unique_lock lock(m); // (1) 공유 큐 보호
q.push(i); // (2) 데이터 생산
} // (3) unlock
this_thread::sleep_for(100ms); // (4) "가끔 들어오는" 상황 가정
}
}

void Consumer() {
while (true) {
unique_lock lock(m); // (1) 매번 락 획득 시도
if (!q.empty()) { // (2) 데이터 없으면 아무것도 안 함
int data = q.front();
q.pop();
cout << data << endl;
}
// (3) 문제: 데이터 없어도 while(true)가 계속 돌아감 = Busy Waiting
}
}

🔴 핵심 문제 2가지

  1. CPU 낭비: 데이터가 없는데도 계속 검사
  1. 락 경쟁: Producer가 push하려고 락 잡을 때 Consumer가 계속 락을 잡으려 들며 경쟁이 커짐

  1. 해결책: Windows Event = 커널 오브젝트

3-1. Event 상태(신호등 비유)

Non-signaled(빨간불): Wait 호출 스레드는 잠든다(Block)

Signaled(파란불): Wait 호출 스레드는 깨어난다(Wake)

3-2. 핵심 API 정확한 의미

API 역할 정확한 기술적 의미

CreateEvent 이벤트 생성 커널 이벤트 오브젝트 생성 후 HANDLE 반환
SetEvent 신호 발생 이벤트를 Signaled로 만들고 대기 스레드 깨움
WaitForSingleObject 대기 이벤트가 Signaled가 될 때까지 스레드를 Block
ResetEvent 신호 해제 (Manual-reset일 때) 다시 Non-signaled로 복구

제미나이 텍스트에서 “SetEvent = 관리자가 신호를 보냄”처럼 느껴질 수 있는데,
SetEvent를 호출하는 건 Producer(혹은 신호를 발생시키는 코드)이고,
커널은 그 신호를 근거로 대기 스레드를 깨우는 주체야.


  1. Auto-reset vs Manual-reset (시험/면접에서도 자주 나옴)

4-1. Auto-reset (bManualReset = FALSE)

SetEvent() → 대기 중 스레드 1개를 깨움

깨우는 순간 이벤트는 자동으로 Non-signaled로 돌아감

“1회 신호 = 1명 통과”에 가까움

단, 카운팅(몇 개 들어왔는지) 기능은 없음 ← 여기서 함정이 생김

4-2. Manual-reset (bManualReset = TRUE)

SetEvent() → 이벤트가 Signaled 상태로 유지

여러 스레드가 기다리고 있으면 우르르 깨어날 수 있음(broadcast 성격)

반드시 적절한 시점에 ResetEvent()로 다시 Non-signaled로 바꿔야 함


  1. (중요) “강의 예제 코드” 그대로 쓰면 생길 수 있는 실전 함정

5-1. 이벤트는 “카운팅”을 못 한다

Auto-reset 이벤트는 신호 1개를 기억하는 큐가 아니야.

즉, Producer가 아래처럼 빠르게 여러 번 SetEvent()를 호출해도,

소비자가 아직 Wait에 들어가지 않았거나,

이벤트가 이미 Signaled 상태라면, 추가 SetEvent()는 누적되지 않는다.

예: 신호 유실(lost wake-up) 시나리오

  1. Producer가 q에 10개 넣고 SetEvent()를 10번 호출
  1. Auto-reset 이벤트는 “10개”를 저장하지 못함
  1. Consumer는 한 번 깨서 1개만 pop하고 다시 Wait로 들어가면
  1. 큐에 9개가 남았는데도 추가 신호가 없으면 그대로 잠들 수 있음

강의 예제는 “개념 입문용”이라 단순화된 형태고, 실전에서는 반드시 보강 패턴이 필요해.


  1. 실전형 구현 1 — “큐가 비어있던 순간만 SetEvent” + “깨어나면 큐를 비울 때까지 처리”

이 패턴은 Event가 카운팅이 아닌 점을 현실적으로 보완하는 가장 흔한 방식이야.

6-1. 핵심 아이디어

Producer:

push하기 전에 큐가 비었으면(0개였다면) → SetEvent()

이미 데이터가 있었다면 → 굳이 SetEvent 안 해도 됨(어차피 Consumer는 깨어서 처리 중이거나 곧 처리할 것)

Consumer:

깨어난 다음에는 큐를 비울 때까지(drain) 처리

큐가 비면 다시 Wait

6-2. 코드(완전판, 줄단위 해설 포함)

#include <windows.h>
#include
#include
#include
#include
#include

using namespace std;

mutex m;
queue q;

HANDLE gEvent = NULL;
atomic gStop{false};

void Producer()
{
for (int i = 0; i < 10000; ++i)
{
bool wasEmpty = false;

    {   // (1) 공유 큐 보호
        lock_guard<mutex> lock(m);

        // (2) "이 push로 인해 Consumer를 깨워야 하는가?" 판단
        //     큐가 비어있던 상태(0개)에서 1개가 되는 순간이면 깨우는 게 의미 있음
        wasEmpty = q.empty();

        // (3) 데이터 생산
        q.push(i);
    } // (4) unlock

    // (5) 큐가 비어있던 순간만 깨움 → 불필요한 SetEvent 남발 방지 + 신호 유실 위험 감소
    if (wasEmpty)
        ::SetEvent(gEvent);

    this_thread::sleep_for(100ms);
}

// (6) 종료 신호: stop 설정 + 한 번 깨워서 Consumer가 빠져나오게 함
gStop.store(true, memory_order_release);
::SetEvent(gEvent);

}

void Consumer()
{
while (true)
{
// (1) 먼저 큐를 비울 수 있으면 비운다(락 안에서 pop만, 처리는 락 밖에서)
while (true)
{
int data = 0;
bool hasItem = false;

        {
            lock_guard<mutex> lock(m);
            if (!q.empty())
            {
                data = q.front();
                q.pop();
                hasItem = true;
            }
        }

        if (!hasItem)
            break;

        // (2) 처리(락 밖)
        cout << "Data: " << data << "\n";
    }

    // (3) 종료 조건: 큐도 비었고 stop이면 종료
    if (gStop.load(memory_order_acquire))
        return;

    // (4) 큐가 비었으니 이벤트가 올 때까지 잠든다(= CPU 거의 0)
    ::WaitForSingleObject(gEvent, INFINITE);

    // (5) 깨어났으면 다시 위 루프에서 큐 drain 시도
}

}

int main()
{
// Auto-reset(FALSE), 초기 Non-signaled(FALSE)
gEvent = ::CreateEvent(NULL, FALSE, FALSE, NULL);
if (!gEvent) return 1;

thread t1(Producer);
thread t2(Consumer);

t1.join();
t2.join();

::CloseHandle(gEvent);
return 0;

}

6-3. 이 버전이 “강의 예제”보다 더 완벽한 이유

신호 유실 위험을 줄임: “0 → 1” 변화에서만 깨움

불필요한 SetEvent 남발을 줄임

Consumer는 깨면 큐를 비울 때까지 처리해서 “신호 1번에 1개만 pop하고 다시 잠드는 문제”를 줄임

종료 처리(stop)까지 포함: 실전에서 꼭 필요


  1. 실전형 구현 2 — “정답에 가까운 구조”: Semaphore(카운팅) + Mutex(큐 보호)

이건 “Event 강의”의 연장선에서 실무적으로 제일 깔끔한 답이야.

Event는 “신호 on/off”만 있음(카운팅 불가)

Producer-Consumer는 “몇 개 들어왔는지(카운트)”가 중요함
→ 그래서 Windows에서는 보통 Semaphore(카운팅 커널 오브젝트)를 쓴다.

강의 다음 챕터가 Condition Variable로 가는 이유도 같은 맥락이야.
“조건+카운트” 문제는 Event만으로는 깔끔하게 안 풀리는 경우가 많거든.

원하면 이 세마포어 버전도 “줄단위 해설 포함”으로 같이 완성해줄게.


  1. Manual-reset 이벤트는 언제 쓰나?

Manual-reset은 “모두 깨우기(broadcast)”가 필요할 때 유용해. 예:

“설정 로딩 완료” 같은 전역 상태가 준비되면,

기다리던 여러 스레드가 한꺼번에 진행해도 되는 상황.

다만 Producer-Consumer 큐처럼 “아이템 단위 동기화”에는

Manual-reset은 과하게 많은 스레드를 깨워서(Thundering Herd) 비효율이 나기 쉬워서 신중히 써야 해.


  1. 최종 Takeaway (이 문서의 결론)

Busy Waiting은 조건이 드문 상황에서 CPU를 낭비한다.

Event는 “대기 스레드를 커널이 재워서 CPU 낭비를 줄이는 도구”다.

하지만 Event는 카운팅이 안 된다 → Producer-Consumer에는 함정이 있다.

그래서 실전에서는

Event를 쓰더라도 “empty→non-empty에서만 SetEvent + drain 처리” 같은 보강이 필요하거나,

더 정답에 가까운 방식(세마포어/조건변수)로 간다.


원하면 다음 중 하나로 이어서 “완전 완전체”로 만들어줄게(너는 확인만 하면 됨, 난 바로 작성 가능):

  1. 세마포어(CreateSemaphore) 기반 Producer-Consumer 완전판 (가장 정석)
  1. std::condition_variable로 동일 구조를 플랫폼 독립적으로 구현 (C++ 표준 정석)
  1. “멀티 Consumer(여러 소비자 스레드)”로 확장했을 때 이벤트/세마포어/조건변수 각각의 차이 비교

어느 루트로 갈까? (나는 개인적으로 1→2 순서 추천)

profile
李家네_공부방

0개의 댓글