프로그래밍/소켓 프로그래밍 입문

Windows 비동기 I/O의 두가지 방식, 이벤트와 콜백

데일리 백수 2025. 1. 25. 20:56

1. 이벤트 기반(Overlapped + Event)

1.1 동작 개요

  1. Overlapped 구조체에 hEvent 필드를 이벤트 객체(CreateEvent로 생성)로 설정.
  2. WriteFile / ReadFile 같은 함수를 Overlapped 모드(마지막 인수로 Overlapped 구조체 전달)로 호출.
  3. 함수는 비동기로 I/O를 시작하고, 즉시 복귀(에러는 ERROR_IO_PENDING일 가능성).
  4. I/O가 완료되면, Windows가 Overlapped 구조체에 지정된 이벤트 객체를 SET 상태로 바꿈.
  5. 애플리케이션은 WaitForSingleObject(hEvent, ...) 또는 WaitForMultipleObjects(...)를 통해 이벤트가 세트될 때까지 대기 → 반환하면 I/O 완료.

1.2 특징

  • 대기 함수(WaitForXXX)를 통해 동기화:
    • WAIT_OBJECT_0 리턴 시점에 “해당 I/O가 끝났다”고 알 수 있음.
  • Overlapped 구조체 안의 콜백 함수 포인터는 없음.
  • 여러 이벤트 핸들을 모아 WaitForMultipleObjects로 처리하면, 한 스레드에서 여러 비동기 I/O 완료를 손쉽게 파악 가능.

1.3 장단점

  • 장점:
    • 구조가 단순(“이벤트가 세트되면 끝났다”).
    • WaitForXXX로 I/O 완료를 기다릴 수 있으므로 직관적.
  • 단점:
    • 대기 방식이므로, “이벤트가 세트될 때까지” 스레드가 블로킹될 가능성이 있음.
    • 스레드가 이벤트를 폴링하거나 대기 시간이 길어질 수 있음.

2. 콜백 기반(Overlapped + Completion Routine)

2.1 동작 개요

  1. WriteFileEx / ReadFileExEx 함수를 사용하여, 마지막 인자로 “Completion Routine(콜백)” 함수 포인터를 전달.
  2. 비동기 요청 후, 함수는 즉시 반환(마찬가지로 ERROR_IO_PENDING 가능).
  3. I/O가 완료되면, Windows가 Alertable Wait 상태인 스레드의 APC Queue에 콜백을 등록.
  4. 스레드가 SleepEx(…, TRUE) 또는 WaitForSingleObjectEx(…, TRUE) 같은 Alertable 모드로 대기하면, APC가 트리거되어 콜백 함수가 실행.

2.2 특징

  • 콜백(Completion Routine)이 자동으로 실행되므로, 이벤트를 WaitForXXX로 확인할 필요 없음.
  • 스레드가 Alertable Wait(“슬립 중이되, APC 발생 시 깨워서 콜백 실행”) 상태를 유지해야 함.
  • Overlapped 구조체는 여전히 필요하지만, hEvent 필드를 굳이 이벤트 객체로 사용하지 않고, “임의의 포인터 보관용”처럼 쓸 수도 있음.

2.3 장단점

  • 장점:
    • 콜백 함수가 자동 실행되므로, 이벤트 대기 로직 생략.
    • I/O 완료 시점에서 원하는 후속 처리를 직접 코드로 작성 가능 → “함수 호출” 느낌.
  • 단점:
    • 스레드가 반드시 Alertable 모드(예: SleepEx(…, TRUE))로 있어야 콜백이 불린다.
    • 코드 구조가 콜백 형태로 분산되어 복잡해질 수 있음.

3. 이벤트 vs 콜백: 무엇이 다른가?

  1. 완료 알림 방식
    • 이벤트 기반: OS가 지정된 이벤트 객체를 세트(SetEvent) → 애플리케이션은 WaitForSingleObject(…) 등으로 블로킹 대기하거나 폴링.
    • 콜백 기반: OS가 콜백 함수를 호출(APC). 스레드는 Alertable Wait. 함수로 직접 제어 흐름이 날아온다.
  2. 스레드 대기 방식
    • 이벤트 기반: 일반적인 WaitForSingleObject(hEvent, INFINITE) 식의 대기.
    • 콜백 기반: SleepEx(timeout, TRUE) 또는 WaitForSingleObjectEx(…, TRUE). → Alertable 모드에서 APC가 호출.
  3. 코드 구조
    • 이벤트: “I/O가 끝나면 이벤트가 SET 됨 → 내가 WaitFor… 뒤에 로직을 작성”
    • 콜백: “I/O가 끝나면 함수가 호출됨(콜백) → 콜백 안에서 후속 처리”

4. 어떤 상황에서 어떤 방식을 택할까?

  1. 이벤트 기반이 편한 경우
    • 이미 여러 이벤트(HANDLE)들(스레드, 다른 I/O, 타이머 등)을 WaitForMultipleObjects로 관리 중인 경우.
    • 일관된 구조로 처리하기 쉽다.
  2. 콜백 기반이 편한 경우
    • 이벤트 대기 없이, “I/O 완료 시 함수가 곧바로 실행”하는 로직이 필요할 때.
    • 여러 I/O 완료 이벤트를 하나의 콜백에서 처리하기 쉽다. → CPU가 다른 작업 하다가도 I/O완료 시점에 콜백이 바로 실행.
  3. 성능 차이:
    • 둘 다 Overlapped(비동기 I/O)이므로, “이벤트 vs 콜백” 자체 성능은 비슷.
    • 코드 구조와 유지보수 측면의 선택일 때가 많음.

5. 콜백에는 대기(wait)가 없을까?

콜백 방식에서도 '기다림'이 전혀 없는 것은 아닙니다. 다만 이벤트 기반과 달리 “블로킹 대기”가 아니라 “Alertable Wait(알터블 웨이트)” 상태에서 OS가 콜백 함수를 호출하는 구조입니다. 즉, 전통적인 WaitForSingleObject(…) 식의 ‘이벤트가 세트될 때까지 가만히 기다리는’ 모델과 달리, 콜백은 특별한 형태의 대기가 필요하다는 점이 다릅니다.

 

1. 이벤트 방식의 ‘대기’ vs 콜백 방식의 ‘대기’

  1. 이벤트 기반
    • 비동기 I/O 완료 시 OS가 이벤트 객체를 SET 상태로 만듦.
    • 애플리케이션은 WaitForSingleObject(hEvent, INFINITE) 같은 함수를 호출해 블로킹 상태에 있다가, 이벤트가 세트되면 반환됨.
    • 이때 스레드는 “이벤트가 세트될 때까지” 실질적으로 기다리는(블로킹) 형태.
  2. 콜백(Completion Routine) 기반
    • 비동기 I/O 완료 시 OS가 해당 스레드에 ‘APC(Asynchronous Procedure Call)’를 등록하고, 그 APC에 콜백 함수가 담김.
    • 스레드가 SleepEx(…, TRUE)나 WaitForSingleObjectEx(…, TRUE) 형태로 Alertable Wait 상태일 때, OS가 바로 콜백 함수를 실행.
    • 즉 “스레드가 ‘Alertable’ 모드로 진입하면 OS가 콜백을 던질 수 있음” → 이벤트처럼 “신호가 오기까지 블로킹”하는 대신, APC 실행이 곧 I/O 완료 처리임.

정리: 콜백 방식도 결국 Alertable Wait(즉, 대기 상태)는 필요하지만, 이벤트처럼 “세트될 때까지 가만히”가 아니라 “콜백이 실행되는 순간 스레드를 깨우면서 함수가 바로 실행”되는 형태입니다.


2. Alertable Wait는 어떻게 다를까?

  • 일반 Sleep(Sleep(ms))
    • 해당 ms 동안 스레드는 완전히 잠들어 APC 호출 불가.
    • I/O가 끝나도 깨우지 못함.
  • Alertable Sleep(SleepEx(ms, TRUE))
    • ms 동안 자되, 이 스레드가 등록한 비동기 I/O가 완료되면, OS가 APC를 큐에 넣고 바로 콜백을 실행.
    • 콜백이 끝난 뒤 스레드가 다시 sleep 상태로 돌아가거나, WAIT_IO_COMPLETION을 반환해 루프를 탈출할 수도 있음.

이렇듯 콜백 방식이라도 “Alertable Wait”로 인한 ‘대기’는 존재합니다. 다만, 전통적인 이벤트 WaitForXXX와 달리, “콜백이 실행됨과 동시에 대기가 깨진다”라는 점이 다릅니다.


3. 그래서, 콜백은 대기가 없는 것인가?

엄밀히 말해, “스레드가 아무 대기도 없이 콜백을 바로 받는” 방식은 아닙니다. 스레드가 Alertable Wait 상태로 들어가 있어야, OS가 I/O 완료 시점에 콜백을 실행해줄 수 있기 때문입니다.

  • 만약 스레드가 절대 Alertable 모드로 가지 않으면, 콜백 자체가 호출되지 않습니다(APC가 큐에 쌓여 있어도 실행 기회가 없음).
  • 따라서 콜백 기반이라고 해서 “대기”가 완전히 사라지는 것은 아니고, “대기의 형태가 바뀐다(블로킹 대기가 아닌 Alertable Wait 상태)”라고 이해하면 됩니다.

4. 비교: 이벤트 vs 콜백 최종 요약

  1. 이벤트 기반
    • I/O 완료 시 이벤트 세트.
    • WaitForSingleObject(...)로 블로킹 대기.
    • 장점: 구조가 직관적, 기존 WaitForMultipleObjects와 조합이 쉬움.
    • 단점: 스레드를 “이벤트 세트까지” 블로킹시키므로, 단순 반복이면 CPU를 제어 흐름으로 돌려주지 않음.
  2. 콜백 기반
    • I/O 완료 시 OS가 APC 큐에 콜백 등록.
    • 스레드가 SleepEx(...) 등의 Alertable Wait 상태일 때, 콜백 실행.
    • 장점: 이벤트 객체 없이, I/O 완료 후 곧바로 함수를 자동 실행 가능.
    • 단점: 스레드가 반드시 Alertable Wait를 해야 함(아니면 콜백 호출 안 됨). 코드 구조가 콜백 형태로 분산.

5. 결론

  • 콜백이라고 해서 대기가 전혀 없는 것이 아니다.
    • 스레드는 여전히 Alertable Wait라는 형태로 “대기”해야, 콜백을 받을 수 있음.
  • 단순 “WaitForSingleObject” 기반의 블로킹 대기와 달리, 콜백은 함수가 곧바로 실행되는 형태여서 좀 더 비동기·이벤트 드리븐에 가깝지만, 내부적으로는 스레드가 alertable 모드로 기다리는 상황이 존재한다고 보면 됩니다.
  • 사용하는 측면에서, 대기 로직(이벤트 vs 콜백) 중 개발 편의코드 구조·프로젝트 요구에 따라 방식을 선택하면 됩니다.