빙응의 공부 블로그
[OSTEP 병행성(concurrency)]이벤트 기반의 병행성(고급) 본문
📝서론
이제까지 쓰레드를 중심으로 병행 프로그램을 설명하였다.
인터넷 서버에서는 다른 스타일로 병행 프로그래밍을 하기도 한다. 이런 스타일을 이벤트 기반의 병행성이라고 부른다.
예를 들어보면 node.js나 스프링의 WebFlux 같은 것들이다. 물론 시작점은 C와 유닉스 시스템이다.
핵심 질문 : 어떻게 스레드 없이 병행 서버를 개발할까?
📝기본 개념 : 이벤트 루프
우리가 다룰 이벤트 기반 병행성은 특정 사건(이벤트)의 발생을 대기한다. 즉 사건이 발생하면, 사건의 종류를 파악한 후 I/O를 요청하고 추후 처리를 위해 다른 이벤트를 발생시킨다. 이게 전부다.
이제 이벤트 루프에 대해 알아보자
while (1) {
events = getEvents();
for (e in events) {
processEvent(e);
}
}
- while 루프 내에서 이벤트를 대기한다.
- 이벤트가 발생하면 하나씩 처리한다.
- 여기서 이벤트를 처리하는 코드를 이벤트 핸들러라고 부른다.
중요한 것은 이벤트의 처리가 시스템이 유일한 작업이기 때문에, 다음에 처리할 이벤트를 결정하는 것이 스케줄링과 동일한 효과를 갖는다. 스케줄링을 제어할 수 있기에 이벤트 기반 방법의 장점 중 하나이다.
하지만 문제도 발생한다. 이벤트가 무슨 이벤트인지 어떻게 판단할까?
📝중요 API: select() (또는 poll())
대부분의 시스템은 select() 또는 poll() 시스템 콜을 기본 API로 제공한다.
해당 인터페이스의 기능은 간단하다. 도착한 I/O들을 검사하는 것이다.
select()를 예로 살펴보자
int select(intnfds,
fd_set *restrictreadfds,
fd_set *restrictwritefds,
fd_set *restricterrorfds,
struct timeval *restrict timeout);
select() 시스템 호출은 소켓, 파일 디스크립터 등에서 발생하는 이벤트를 비동기적으로 감지하는데 사용됩니다. 주로 네트워크 프로그래밍에서 서버가 여러 클라이언트와 비동기적으로 통신할 때 유용하게 사용됩니다. 이 호출은 지정된 시간 내에 주어진 파일 디스크립터 집합들에 대해 읽기, 쓰기, 예외 발생 여부를 확인하고, 이를 바탕으로 적절한 처리 작업을 진행할 수 있게 합니다.
💡Spring WebFlux는 어떻게 동작할까?
스프링의 웹플럭스는 웹 기반으로 들어오는 특정 경로(예: /process)와 HTTP 메서드(예: GET, POST)를 통해 구분합니다.
- HTTP 요청 수신: 들어오는 요청은 특정 경로(예: /process)와 HTTP 메서드(예: GET, POST)를 포함합니다.
- 라우팅: RouterFunction이 이를 분석하고 적절한 HandlerFunction을 호출합니다.
- 이벤트 판별: 요청 내에서 이벤트를 판별하는 로직을 구현하여 이벤트가 무엇인지 결정합니다. 예를 들어, 쿼리 파라미터나 헤더 값으로 이벤트 종류를 구분할 수 있습니다.
- 비즈니스 로직 처리: 이벤트 종류에 따라 적절한 비즈니스 로직을 실행합니다.
- 응답 반환: 처리 결과를 클라이언트에게 응답합니다.
📝왜 간단한가? 락이 필요없음
단일 CPU를 사용하는 이벤트 기반의 응용 프로그램에서는 병행 프로그램을 다룰 때 나타났던 문제들이 더 이상 보이지 않습니다.
그 이유는 매순간의 단 하나의 이벤트만 다루기에 락을 획득하거나 해제해야 할 필요가 없습니다.
이벤트 기반의 서버는 단 하나의 쓰레드를 가지고 이기 때문에 인터럽트가 걸릴 일이 없습니다.
📝문제 : 블로킹 시스템 콜(Blocking System Call)
이제까지는 이벤트 기반 프로그래밍의 좋은 점만 알아보았다.
그러나 차단될 수도 있는 시스템 콜을 불러야 하는 이벤트가 있다면 문제가 발생한다.
예를 들어 디스크에서 데이터를 읽어서 그 내용을 사용자에게 전달하는 요청을 생각해보자
- 이벤트 핸들러가 블로킹 콜을 호출한다.
- 서버 전체가 그 일을 처리하기 위해 명령어가 끝날 때까지 다른 것들을 모두 차단한다.
- 이벤트 루프가 블록되면 시스템은 쉬게된다.
- 심각한 자원 낭비가 발생한다.
그렇기에 이벤트 기반 접근법은 블로킹 콜이 있어선 안된다.
📝해법 : 비동기 I/O
언급한 한계를 극복하기 위해 I/O 요청을 디스크로 내려 보낼 수 있는 비동기 I/O 방식을 개발하였다.
이 인터페이스는 프로그램이 I/O 요청을 하면 I/O 요청이 끝나기 전에 제어권을 다른 호출자에게 돌려주는 것이 가능해졌다.
하지만 한가지 문제가 있다. I/O가 종료되었다는 것을 어떻게 알 수 있을까?
대기중인 모든 비동기 I/O는 주기적으로 시스템 콜을 통해 시스템에 폴링(poll) 하여 해당 I/O 가 완료되었는지 확인할 수 있다.
일일이 다 검사하면 시간이 오래걸리기에 어떤 시스템들은 인터럽트 기반의 접근법을 제공한다. 시그널(signal) 을 사용하여 완료를 알려준다.
비동기 I/O가 없는 시스템에서는 제대로 된 이벤트 기반의 접근법을 구현할 수 없다. 그렇기에 현대에는 대기 중인 I/O를 처리하기 위해 쓰레드 풀을 이용한 하이브리드 기법을 제안했다.
📝또 다른 문제 : 상태 관리
이벤트 기반 접근법의 또 다른 문제점은 전통적인 쓰레드 기반 코드보다 복잡하다는 것입니다.
상태 관리 문제의 본질
이벤트 기반 접근법에서는 상태를 관리하는 방식이 달라집니다. 비동기 I/O 작업을 처리하는 이벤트 핸들러는 쓰레드가 아닌 이벤트 루프에서 실행됩니다. 이 경우, 비동기 I/O가 완료되었을 때 사용할 프로그램 상태를 미리 정의하고 관리해야 하는데, 이를 수동으로 관리해야 합니다.
쓰레드 기반 시스템은 각 쓰레드가 고유 스택을 가지고 있기에 작업 수행할 때 자동으로 포함됩니다.
📝이벤트 기반 접근법의 어려움
이벤트 기반 시스템은 비동기적 처리로 많은 이점을 제공하지만, 여러 가지 문제점도 동반할 수 있습니다. 특히, 멀티CPU 환경에서 성능을 최적화하려면 해결해야 할 어려운 문제들이 있습니다. 주요 어려움은 다음과 같습니다:
1. 멀티CPU 환경에서의 동기화 문제 💻
단일 CPU에서 멀티 CPU로 변경되면, 이벤트 기반 접근법의 단순함은 복잡해집니다. 여러 CPU를 활용하기 위해서는 이벤트 핸들러를 병렬적으로 실행해야 하는데, 이로 인해 동기화 문제(예: 임계 영역)가 발생할 수 있습니다.
- 동기화 문제를 해결하려면 락(lock) 등의 동기화 메커니즘을 사용해야 하지만, 멀티코어 시스템에서는 락을 사용하는 방식은 성능 저하를 초래할 수 있습니다.
- 이런 이유로 멀티코어 시스템에서는 이벤트 기반 처리 방식이 더 이상 효과적으로 작동하지 않을 수 있습니다. ⚠️
2. 페이지 폴트로 인한 차단 문제 🔄
이벤트 기반 시스템은 논블로킹(non-blocking) 방식으로 설계되지만, 페이지 폴트(paging)와 같은 시스템 이벤트에서는 차단(blocking)이 발생할 수 있습니다.
- 페이지 폴트가 발생하면, 그 처리가 완료될 때까지 서버는 진행할 수 없게 됩니다.
- 비차단 방식이라 할지라도, 페이지 폴트와 같은 내재적인 차단은 피할 수 없으며, 이는 성능 저하를 초래할 수 있습니다.
- 페이지 폴트가 자주 발생하는 시스템에서는 성능에 심각한 영향을 미칠 수 있습니다.
3. 루틴 동작의 변화에 따른 관리 어려움 🔄
이벤트 기반 시스템에서는 루틴의 동작 방식이 자주 변화할 수 있습니다. 예를 들어, 루틴이 비차단 방식에서 차단 방식으로 변경되면, 해당 루틴을 호출하는 이벤트 핸들러도 새로운 특성에 맞게 변경해야 합니다.
- 이렇게 루틴의 동작이 계속 변하면 관리가 어려워집니다. 개발자는 각 이벤트가 사용하는 API 문법의 변경을 주의 깊게 살펴야 합니다.
- 차단(block) 방식은 이벤트 기반 시스템에서는 치명적인 문제로 작용할 수 있습니다.
4. 비동기 디스크 I/O와 네트워크 I/O의 일관성 문제 💾🌐
비동기 디스크 I/O는 대부분의 플랫폼에서 지원되지만, 네트워크 I/O는 아직까지 일관성 있게 적용되지 않는 경우가 많습니다. 예를 들어, 네트워크 요청을 처리할 때는 select()를 사용하고, 디스크 I/O는 AIO(Asynchronous I/O)를 사용하여 처리하는 경우가 많습니다.
- 모든 I/O 처리에 대해 select()를 사용하는 것이 이상적이지만, 실제로는 네트워크와 디스크 I/O 처리가 서로 다른 방식으로 처리되고 있어, 일관성 있는 구현이 어려운 상황입니다.
📝요약
💡 기본 개념
- 이벤트 루프(event loop): 이벤트가 발생할 때까지 대기하고, 이벤트가 발생하면 해당 이벤트를 처리하는 구조.
- 이벤트를 처리하는 코드를 이벤트 핸들러라고 부름.
- select() / poll() 등의 시스템 호출을 통해 비동기적으로 I/O 이벤트를 감지.
while (1) {
events = getEvents();
for (e in events) {
processEvent(e);
}
}
👍 장점
- 단일 스레드 기반이므로 락(lock) 이 필요 없음.
- 인터럽트 없이 안정적으로 작동.
- CPU 자원을 효율적으로 사용.
⚠️ 문제점 및 어려움
- 멀티CPU 환경의 동기화 문제
- 멀티코어 환경에서 이벤트 핸들러 병렬 실행 필요 → 락 사용 불가피 → 성능 저하.
- 페이지 폴트로 인한 차단
- 이벤트 루프에서 페이지 폴트 발생 시 루프가 멈춤 → 비차단 설계의 의미 퇴색, 성능 하락 가능.
- 루틴 동작의 변화
- 비차단 루틴이 차단 방식으로 바뀌면 이벤트 핸들러도 변경 필요 → 관리 복잡도 증가.
- I/O 처리의 일관성 문제
- 네트워크는 select(), 디스크는 AIO 등 각기 다른 방식 사용 → 일관성 부족.
- 플랫폼마다 비동기 I/O 지원 방식이 다름.
✅ 보완 방법
- 비동기 I/O 사용 → 작업 완료 전에 제어권 반환 가능.
- 인터럽트/Signal 기반 알림 또는 스레드 풀 기반 하이브리드 처리로 병목 해결.
'CS > 운영체제' 카테고리의 다른 글
[OSTEP 영속성(persistence)]하드 디스크 드라이브(HDD) (0) | 2025.06.02 |
---|---|
[OSTEP 영속성(persistence)]I/O 장치 (1) | 2025.05.27 |
[OSTEP 병행성(concurrency)]병행성 관련 오류 (0) | 2025.05.06 |
[OSTEP 병행성(concurrency)]세마포어(semaphore) (0) | 2025.05.04 |
[OSTEP 병행성(concurrency)]컨디션 변수 (0) | 2025.05.04 |