운영체제 아주 쉬운 세가지 이야기 12주차(이벤트 기반의 병행성(고급))

2025. 11. 25. 20:01개념 공부

0. 이 챕터가 해결하려는 문제: “왜 굳이 스레드 말고 이벤트?”

  • 스레드 기반 동시성은 락 누락/데드락/경쟁조건 등 “정확성”이 어렵습니다.
  • 또한 스레드 방식은 “지금 CPU가 뭘 실행할지”를 OS 스케줄러에 맡기므로, 개발자가 스케줄링/처리 순서 제어를 하기 어렵습니다. 
  • 이벤트 방식은 단일 스레드에서 이벤트를 하나씩 처리하므로, 어떤 이벤트를 다음에 처리할지(=스케줄링)를 앱이 직접 결정할 수 있습니다.
 

1. 핵심 모델: Event Loop(이벤트 루프)

이벤트 기반 서버의 “뼈대”는 아래처럼 단순합니다. 루프가 이벤트를 기다렸다가, 이벤트 목록을 받아 하나씩 처리(handler)합니다. 

// Canonical event loop (pseudocode)
while (1) {
  events = getEvents();
  for (e in events)
    processEvent(e);
}

여기서 중요한 해석

  • processEvent(e) 가 실행되는 동안 시스템에서 “실질적으로 진행되는 일”은 이것 하나뿐 → 그래서 다음에 뭘 할지 = 스케줄링입니다.
  • 스레드가 많아져도 “동시에 실행되는 것처럼 보이는” 이유는 OS가 번갈아 스케줄링하기 때문인데, 이벤트 방식은 그 스케줄링을 앱이 직접 쥡니다(대신 제약이 큼).
 

2. 이벤트를 어떻게 “받아오나?”: select() / poll()

이벤트 루프에서 getEvents() 역할을 OS가 도와주는 대표 API가 select() / poll() 입니다. 이들은 “지금 읽을/쓸 준비가 된 FD(소켓 등)가 있나?”를 확인하게 해 줍니다.

select()의 감각

  • read ready: 읽을 데이터가 도착(예: inbound 패킷)해서 처리 가능
  • write ready: 보내도 되는 상태(예: outbound queue가 꽉 차지 않음)
  • timeout: NULL이면 준비될 때까지 블록, 0이면 즉시 리턴(폴링 느낌) 등 다양한 운영이 가능

책의 예시 코드는 “fd_set을 만들고 → select로 준비된 FD를 받고 → 준비된 FD만 처리” 흐름입니다.

// Simple select() usage (simplified from the chapter)
while (1) {
  fd_set readFDs;
  FD_ZERO(&readFDs);
  for (fd = minFD; fd < maxFD; fd++)
    FD_SET(fd, &readFDs);

  select(maxFD+1, &readFDs, NULL, NULL, NULL);

  for (fd = minFD; fd < maxFD; fd++)
    if (FD_ISSET(fd, &readFDs))
      processFD(fd);
}
 

3. (중요) Blocking을 하면 왜 망하나?

이벤트 기반 서버의 “규칙”은 딱 하나로 요약됩니다: 이벤트 핸들러 안에서 블로킹 호출을 절대 하면 안 된다.

왜?

  • 스레드 기반: 어떤 스레드가 read() 때문에 잠들어도, 다른 스레드가 계속 진행 → I/O와 계산이 자연스럽게 겹침 
  • 이벤트 기반(단일 루프): 핸들러가 open()/read() 같은 블로킹 I/O를 호출하면, 서버 전체가 멈춤 → 그 동안 다른 이벤트를 처리할 주체가 없음
 

4. 해결: Asynchronous I/O(AIO)

블로킹 문제를 피하려면, 디스크 같은 느린 I/O는 비동기(Asynchronous)로 요청만 던지고 즉시 돌아와야 합니다. 

AIO의 큰 그림

  1. 요청 정보(aiocb) 채우기: fd, offset, buffer 주소, bytes 등
  2. aio_read() 호출: 성공하면 “요청만 걸고” 바로 리턴
  3. 완료 확인: aio_error()로 완료 여부 확인(완료면 0, 아니면 EINPROGRESS)

현실의 고민: 완료 확인을 매번 polling 해야 해?

  • 요청이 수십/수백 개면, 매번 다 확인하는 건 고통스럽습니다.
  • 그래서 어떤 시스템은 시그널(UNIX signals) 같은 “인터럽트 기반 알림”으로 AIO 완료를 알려줍니다
  • 또는 현실적으로 “네트워크는 이벤트로, 디스크는 스레드풀로” 같은 하이브리드 구조도 연구/실무에서 씁니다.
 

5. 이벤트 방식의 진짜 난점: 상태 관리(Manual Stack Management)

스레드 방식은 “다음에 해야 할 일”에 필요한 상태가 스레드 스택에 자연스럽게 남아 있습니다. 하지만 이벤트 방식은 핸들러가 비동기 I/O를 걸고 리턴해버리면, 다음 이벤트(완료 이벤트)가 왔을 때 쓸 상태를 직접 저장/복원해야 합니다. 이걸 책에서는 manual stack management라고 부릅니다.

스레드 기반은 왜 쉬운가?

예를 들어 “파일 fd에서 읽고 → 소켓 sd로 쓰기”는 스레드에서는 아래처럼 직관적입니다. 읽기가 끝나면 스택에 sd가 그대로 있으니까요. 

// Thread-based (concept)
int rc = read(fd, buffer, size);
rc = write(sd, buffer, size);

이벤트 기반은 왜 어려운가?

AIO로 읽기를 “요청만” 하고 돌아왔다는 건, 현재 함수/스택 프레임이 끝난다는 뜻입니다. 나중에 I/O 완료 이벤트가 왔을 때, “어느 소켓(sd)으로 write 해야 하는지” 같은 상태를 어디선가 찾아와야 해요.

해법: Continuation(컨티뉴에이션)

  • 핵심 아이디어: “이 작업을 마무리하는 데 필요한 정보를 구조체/테이블에 기록해두고, 완료 이벤트가 오면 그걸 찾아 마저 처리한다.” 
  • 책의 예: (fd → sd) 매핑을 어딘가(예: 해시테이블)에 저장해 두었다가, 디스크 I/O 완료 시 fd로 continuation을 찾아 sd를 얻고 write 수행

즉, 이벤트 기반 코드는 점점 “A 요청 시작 핸들러”와 “A 요청 완료 핸들러”처럼 로직이 조각조각 찢어지는 경향이 생깁니다. 이게 흔히 말하는 “콜백 지옥 / 상태 머신 지옥”의 근본 원인 중 하나입니다(책에서는 API 의미가 바뀌면 핸들러를 또 찢어야 한다고 경고). 

 

6. “락이 필요 없어져서 좋은데요?” — 장점의 정확한 범위

단일 CPU + 단일 이벤트 루프라면, 한 번에 한 이벤트만 처리하므로 동시에 임계영역에 들어올 주체가 없어서 락이 불필요해집니다. 

하지만 이 말은 “이벤트 방식이면 무조건 락이 필요 없다”가 아닙니다: 멀티코어에서 성능을 뽑으려고 이벤트 핸들러를 병렬로 돌리려는 순간, 다시 동기화(락/임계구역)가 필요해진다고 책에서 분명히 말합니다.

 

7. 이벤트 기반이 여전히 어려운 지점(실무 인사이트 포인트)

  • 멀티코어: 여러 CPU를 쓰려 하면 결국 병렬성 → 동기화 이슈 귀환
  • 페이징/페이지 폴트: 핸들러가 page fault를 일으키면 “명시적으로 블로킹 호출을 안 했는데도” 멈출 수 있음 
  • API 의미 변화 리스크: 어떤 함수가 나중에 blocking 성격으로 바뀌면, 해당 핸들러를 다시 쪼개야 함(유지보수 난이도) 
  • 비동기 I/O 통합 난이도: 네트워크는 select, 디스크는 AIO 등 인터페이스가 “깔끔하게 하나로” 합쳐지지 않는 경우가 많음

9) 미니 퀴즈(자기 점검)

  1. 이벤트 기반 서버에서 “락이 덜 필요해지는” 이유를 한 문장으로 설명해보세요.
    정답 보기
    단일 이벤트 루프에서 한 번에 하나의 이벤트 핸들러만 실행되므로, 같은 공유 상태를 동시에 건드릴 경쟁자가 사라져 기본적으로 락이 필요 없어집니다.
    단, 멀티코어 활용을 위해 핸들러를 병렬화하면 다시 동기화 문제가 돌아옵니다. 
  2. 이벤트 기반 서버에서 “블로킹 호출 금지”가 왜 치명적인 규칙인가요?
    정답 보기
    이벤트 기반은 대개 단일 루프/단일 실행 흐름이어서, 핸들러가 블로킹 I/O를 호출하면 서버 전체가 멈춰 다른 이벤트를 처리할 주체가 사라집니다.
  3. select()가 서버에 주는 “이벤트”의 정체는 무엇인가요?
    정답 보기
    “읽기/쓰기/에러 처리 준비가 된 파일 디스크립터 집합”입니다. 준비된 FD만 골라 핸들러를 돌리게 해 줍니다.
  4. AIO에서 aio_read()aio_error()는 각각 왜 필요한가요?
    정답 보기
    aio_read()는 디스크 I/O를 “요청만” 걸고 즉시 돌아오기 위해 필요하고, aio_error()는 요청이 끝났는지(완료/진행중)를 확인하기 위해 필요합니다.
  5. “manual stack management”가 정확히 무엇을 의미하나요?
    정답 보기
    비동기 I/O를 걸고 핸들러가 끝나면 스택 프레임이 사라지므로, 나중에 완료 이벤트가 왔을 때 필요한 상태(예: 어느 sd로 write할지)를 직접 저장/복원해야 하는 작업을 말합니다.
 

10. 한 줄 요약

  • 이벤트 기반 동시성은 이벤트 루프가 select/poll 등으로 “준비된 I/O 이벤트”를 받고, 핸들러를 순차 실행하며 스케줄링 제어를 얻는 방식이다.
  • 대신 블로킹 호출 금지, 비동기 I/O(AIO), continuation 기반 상태 관리 같은 복잡성이 따라온다.