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

Windows Overlapped I/O로 비동기 파일 쓰기: 이벤트 방식

데일리 백수 2025. 1. 24. 21:38

아래 예제는 Windows에서 Overlapped I/O(비동기 I/O)를 사용해 파일에 동시에 여러 번의 쓰기 요청을 시도하고, 각각의 요청 완료를 이벤트를 통해 기다리는 구조입니다. 일반적으로 ‘fopen‘같은C라이브러리함수`fopen` 같은 C 라이브러리 함수는 비동기 처리를 지원하지 않으므로, 이 방식에서는 Win32 API의 CreateFile과 WriteFile을 사용합니다.

 

1. Overlapped I/O 기초 개념

  1. Overlapped 구조체
    • 비동기 I/O 요청 시, I/O 작업 상태와 결과를 운영체제가 보관할 공간.
    • OVERLAPPED 안에는 Offset(파일 쓰기 시작 위치), hEvent(이벤트 핸들) 등이 있음.
  2. 비동기(Overlapped) Write
    • WriteFile 함수를 호출해도 즉시 반환.
    • 실제 디스크 쓰기는 운영체제(커널)가 백그라운드에서 처리.
    • 완료 시, OVERLAPPED.hEvent에 연결된 이벤트가 SET됨.
  3. 이벤트 객체 (Event)
    • 비동기 I/O 완료 시점을 알려줌.
    • CreateEvent로 생성 → WriteFile 시점에 OVERLAPPED.hEvent에 연결 → 완료 시 OS가 SET.
  4. WaitForMultipleObjects
    • 여러 이벤트 핸들을 기다리는 함수.
    • 이벤트가 하나라도 SET 상태가 되면 반환(또는 모두 기다리는 모드).
    • 해당 인덱스로 어떤 I/O가 완료되었는지 알 수 있음.

2. 코드 흐름 요약

// 1. 비동기 파일 열기
HANDLE hFile = ::CreateFile(
   _T("TestFile.txt"),
   GENERIC_WRITE,      // 쓰기 모드
   0,                  // 공유하지 않음
   NULL,
   CREATE_ALWAYS,      // 파일이 이미 있으면 덮어쓰기
   FILE_ATTRIBUTE_NORMAL | FILE_FLAG_OVERLAPPED,  // Overlapped(비동기) 모드
   NULL
);

// 2. 이벤트와 OVERLAPPED 구조체 생성
OVERLAPPED aOl[3] = {0};
HANDLE aEvt[3] = {0};
for (int i = 0; i < 3; ++i) {
    aEvt[i] = ::CreateEvent(NULL, FALSE, FALSE, NULL);
    aOl[i].hEvent = aEvt[i];
}

// 3. 각 Overlapped I/O 요청
aOl[0].Offset = 0;              // 파일 처음
aOl[1].Offset = 1024 * 1024 * 128;   // 128MB 지점
aOl[2].Offset = 16;             // 16바이트 위치

for (int i = 0; i < 3; ++i) {
    printf("%d번째 중첩된 쓰기 시도.\n", i);
    ::WriteFile(hFile, "0123456789", 10, &dwRead, &aOl[i]);
    if (::GetLastError() != ERROR_IO_PENDING)
        exit(0); // 비동기 요청 실패 시 종료
}

// 4. 3번의 비동기 쓰기가 끝날 때까지 대기
DWORD dwResult = 0;
for (int i = 0; i < 3; ++i) {
    dwResult = ::WaitForMultipleObjects(3, aEvt, FALSE, INFINITE);
    printf("-> %d번째 쓰기 완료.\n", dwResult - WAIT_OBJECT_0);
}

// (생략) 파일 닫기, 자원 정리

요점 정리

  1. CreateFile
    • FILE_FLAG_OVERLAPPED 플래그로 비동기 쓰기(Overlapped I/O)를 활성화.
    • 일반적인 fopen은 비동기를 지원하지 않으므로 사용 불가.
  2. 이벤트 생성
    • I/O가 완료될 때 알림을 받을 이벤트 객체 3개 생성.
    • OVERLAPPED 구조체 배열(aOl)의 hEvent 필드에 각각 연결.
  3. Offset 설정
    • aOl[i].Offset = ...로 파일 쓰기 시작 위치 지정.
    • 이 위치에 따라 파일이 확 늘어나거나, 특정 지점에 데이터를 기록.
  4. WriteFile 비동기 호출
    • 세 번의 쓰기 요청을 연속으로 진행.
    • 즉시 반환되며, 실제 쓰기는 OS 내부 큐에서 스케줄링.
  5. WaitForMultipleObjects
    • 한 번에 3개의 이벤트를 기다림.
    • 이벤트가 SET되면(= 쓰기 요청 완료) 반환, 이를 3번 반복해 모든 쓰기 완료를 감시.

3. 주요 개념 상세 설명

3.1 비동기 I/O 요청과 큐

  • 비동기 Write:
    • WriteFile(..., &aOl[i]) 호출 시, OS는 해당 요청을 큐에 등록.
    • 함수는 즉시 리턴(에러 코드는 ERROR_IO_PENDING).
  • OS 내부 루프:
    • 큐에서 요청을 꺼내 실제 디스크에 쓰기 수행.
    • 완료 시점에 aOl[i].hEvent를 SET.

3.2 이벤트와 WaitForMultipleObjects

  • 이벤트(Event):
    • OS가 I/O 완료 시 SetEvent 호출로 알리는 동기화 객체.
  • WaitForMultipleObjects(3, aEvt, FALSE, INFINITE):
    • 이벤트 배열 aEvt[3] 중 어느 하나가 SET되면 즉시 복귀( FALSE = WaitAny 모드 ).
    • 반환값은 WAIT_OBJECT_0 + 인덱스.
    • 이 인덱스를 사용해 어떤 요청이 끝났는지 알 수 있음.

3.3 파일 Offset과 파일 크기 증가

  • aOl[0].Offset = 0: 파일 시작 부분에 10바이트 기록.
  • aOl[1].Offset = 128MB: 파일이 아직 128MB 크기에 미치지 않았어도, OS는 그 지점을 작성 → 파일이 확장됨.
  • aOl[2].Offset = 16: 16바이트 위치에 “0123456789”를 기록 → 앞부분에 빈 공간(‘\0’ 등) 생길 수 있음.

4. 동기 vs 비동기: 왜 비동기가 필요한가?

4.1 동기 방식의 문제

  • 요청 후 완료까지 대기(블로킹).
  • 여러 개의 큰 I/O 작업을 동시에 하려면 스레드가 여러 개 필요 → 성능 저하, 자원 낭비.

4.2 비동기 방식의 이점

  • CPU와 I/O 분리:
    • CPU는 다른 작업(연산, UI 등)을 진행, I/O는 OS 내부 스케줄링.
  • 고성능:
    • I/O가 많은 서버 환경에서 대규모 동시 처리 가능.

5. 결론 및 요약

  • CreateFile + FILE_FLAG_OVERLAPPED: 비동기(Overlapped) I/O를 활성화하는 핵심.
  • WriteFile(... Overlapped ...): 요청이 큐에 등록되어 즉시 반환, 실제 쓰기는 백그라운드에서 진행.
  • 이벤트(Event) 객체: 각 요청 완료 시 OS가 SetEvent로 알림 → WaitForMultipleObjects 등으로 감시.
  • Offset 설정: 같은 파일에 여러 위치를 동시에 쓰는 예시. 파일 크기를 확장하는 작업은 OS가 처리.

이처럼 Overlapped I/O(비동기)는 고성능 파일 I/O나 대규모 서버에서 필수적인 기술입니다. 파일 쓰기를 여러 번 동시에 실행해도, OS가 큐에서 스케줄링하여 병렬로 처리하고 완료 이벤트로 알릴 수 있기 때문에, CPU 사용 효율쓰레드 최소화라는 측면에서 강력한 이점을 제공합니다.