-
Event Loop: GeventKnowledge Base/Python 2025. 12. 24. 20:58

핵심 질문: Event Loop란 무엇이며, 왜 서로 다른 Event Loop는 충돌할까?
Event Loop는 단일 스레드에서 여러 I/O 작업을 동시에 처리하는 메커니즘이다.
┌─────────────────────────────────────────────────────────────┐ │ Event Loop의 본질 │ ├─────────────────────────────────────────────────────────────┤ │ │ │ "루프 안에서 I/O 이벤트를 감지하고, │ │ 해당 이벤트에 등록된 콜백을 실행한다" │ │ │ │ while True: │ │ events = wait_for_io_events() # OS에 위임 │ │ for event in events: │ │ callback = get_callback(event) │ │ callback() │ │ │ └─────────────────────────────────────────────────────────────┘
1. OS 수준 I/O Multiplexing
Event Loop의 핵심은 OS가 제공하는 I/O Multiplexing API에 있다.
1.1 I/O Multiplexing이란?
여러 개의 파일 디스크립터(소켓, 파일 등)를 하나의 스레드에서 동시에 감시하는 기술.
┌─────────────────────────────────────────────────────────────┐ │ I/O Multiplexing 개념 │ ├─────────────────────────────────────────────────────────────┤ │ │ │ 전통적 방식 (Blocking I/O): │ │ ─────────────────────────── │ │ Thread 1: socket.recv() ████████████████ (블로킹) │ │ Thread 2: socket.recv() ████████████████ (블로킹) │ │ Thread 3: socket.recv() ████████████████ (블로킹) │ │ │ │ → 연결당 1 스레드 필요 (C10K 문제) │ │ │ │ ─────────────────────────────────────────────────────── │ │ │ │ I/O Multiplexing: │ │ ───────────────── │ │ Thread 1: epoll_wait([fd1, fd2, fd3]) │ │ │ │ │ ▼ │ │ [fd1 ready] → process fd1 │ │ [fd3 ready] → process fd3 │ │ ... │ │ │ │ → 1 스레드로 다수의 연결 처리 │ │ │ └─────────────────────────────────────────────────────────────┘1.2 OS별 I/O Multiplexing API
API OS 특징 시간 복잡도 select 모든 OS 가장 오래됨, FD 1024개 제한 O(n) poll POSIX select 개선, FD 제한 없음 O(n) epoll Linux Edge/Level trigger, 효율적 O(1) kqueue BSD/macOS epoll과 유사 O(1) IOCP Windows Completion Port 모델 O(1) 1.3 epoll 동작 원리 (Linux)
┌─────────────────────────────────────────────────────────────┐ │ epoll 동작 원리 │ ├─────────────────────────────────────────────────────────────┤ │ │ │ 1. epoll 인스턴스 생성 │ │ epfd = epoll_create() │ │ │ │ 2. 감시할 FD 등록 │ │ epoll_ctl(epfd, EPOLL_CTL_ADD, socket_fd, events) │ │ │ │ 3. 이벤트 대기 (블로킹) │ │ n = epoll_wait(epfd, events, max_events, timeout) │ │ │ │ │ │ ← 커널이 ready FD를 알려줌 │ │ ▼ │ │ │ │ 4. Ready FD 처리 │ │ for i in range(n): │ │ fd = events[i].data.fd │ │ handle_event(fd) │ │ │ │ ┌────────────────────────────────────────────────────┐ │ │ │ 핵심: 커널이 Ready 상태인 FD만 반환 │ │ │ │ → 애플리케이션은 모든 FD를 순회할 필요 없음 │ │ │ │ → O(1) 시간 복잡도 │ │ │ └────────────────────────────────────────────────────┘ │ │ │ └─────────────────────────────────────────────────────────────┘1.4 Python에서의 I/O Multiplexing
# Python select 모듈 (저수준) import select # epoll 사용 예시 (Linux) epoll = select.epoll() epoll.register(socket_fd, select.EPOLLIN) while True: events = epoll.poll() # 블로킹 for fd, event in events: if event & select.EPOLLIN: data = connections[fd].recv(1024)
2. Event Loop 구조
2.1 기본 Event Loop 구조
┌─────────────────────────────────────────────────────────────┐ │ Event Loop 아키텍처 │ ├─────────────────────────────────────────────────────────────┤ │ │ │ ┌─────────────────────────────────────────────────────┐ │ │ │ Event Loop │ │ │ │ ┌─────────────────────────────────────────────────┐│ │ │ │ │ Main Loop ││ │ │ │ │ ││ │ │ │ │ ┌──────────┐ ┌──────────┐ ┌──────────┐ ││ │ │ │ │ │ Timer │ │ I/O │ │ Signal │ ││ │ │ │ │ │ Queue │ │ Queue │ │ Queue │ ││ │ │ │ │ └────┬─────┘ └────┬─────┘ └────┬─────┘ ││ │ │ │ │ │ │ │ ││ │ │ │ │ └───────────────┼───────────────┘ ││ │ │ │ │ ▼ ││ │ │ │ │ ┌──────────────┐ ││ │ │ │ │ │ Ready Queue │ ││ │ │ │ │ └──────┬───────┘ ││ │ │ │ │ │ ││ │ │ │ │ ▼ ││ │ │ │ │ ┌──────────────┐ ││ │ │ │ │ │ Execute │ ││ │ │ │ │ │ Callbacks │ ││ │ │ │ │ └──────────────┘ ││ │ │ │ └─────────────────────────────────────────────────┘│ │ │ └─────────────────────────────────────────────────────┘ │ │ │ │ 동작 순서: │ │ 1. Timer 만료 체크 → Ready Queue │ │ 2. I/O 이벤트 체크 (epoll_wait) → Ready Queue │ │ 3. Signal 체크 → Ready Queue │ │ 4. Ready Queue의 콜백 실행 │ │ 5. 반복 │ │ │ └─────────────────────────────────────────────────────────────┘2.2 Event Loop의 핵심 컴포넌트
컴포넌트 역할 예시 I/O Watcher FD 이벤트 감시 소켓 읽기/쓰기 Timer 시간 기반 이벤트 setTimeout, setInterval Signal Handler OS 시그널 처리 SIGTERM, SIGINT Callback Registry 이벤트-콜백 매핑 fd → handler Ready Queue 실행 대기 콜백 FIFO 큐
3. asyncio Event Loop
3.1 asyncio의 Event Loop 구현
Python asyncio는 selectors 모듈을 사용하여 OS별 최적의 I/O Multiplexing을 선택한다.
# asyncio는 자동으로 최적의 selector 선택 # Linux: EpollSelector # macOS: KqueueSelector # Windows: ProactorEventLoop (IOCP) import asyncio async def main(): # Event Loop 내부에서 실행 await asyncio.sleep(1) # Timer 이벤트 asyncio.run(main()) # Event Loop 생성 및 실행3.2 asyncio Event Loop 동작
┌─────────────────────────────────────────────────────────────┐ │ asyncio Event Loop 동작 │ ├─────────────────────────────────────────────────────────────┤ │ │ │ asyncio.run(main()) │ │ │ │ │ ▼ │ │ ┌─────────────────────────────────────────────────────┐ │ │ │ loop = asyncio.new_event_loop() │ │ │ │ asyncio.set_event_loop(loop) │ │ │ │ │ │ │ │ loop.run_until_complete(main()) │ │ │ │ │ │ │ │ │ ▼ │ │ │ │ ┌───────────────────────────────────────────────┐ │ │ │ │ │ _run_once() │ │ │ │ │ │ │ │ │ │ │ │ 1. self._scheduled (Timer) 처리 │ │ │ │ │ │ 2. self._selector.select(timeout) │ │ │ │ │ │ → epoll_wait / kqueue │ │ │ │ │ │ 3. 콜백 실행 │ │ │ │ │ │ 4. 반복 │ │ │ │ │ └───────────────────────────────────────────────┘ │ │ │ └─────────────────────────────────────────────────────┘ │ │ │ │ 핵심 특징: │ │ • Python 레벨 구현 (순수 Python + selectors) │ │ • async/await 문법과 통합 │ │ • 스레드당 하나의 Event Loop │ │ │ └─────────────────────────────────────────────────────────────┘3.3 asyncio 제약사항
# ❌ 중첩된 Event Loop 실행 불가 import asyncio async def outer(): asyncio.run(inner()) # RuntimeError! async def inner(): await asyncio.sleep(1) # 에러: "Cannot run the event loop while another loop is running"
4. Gevent/Eventlet Event Loop
4.1 libev/libuv 기반
Gevent와 Eventlet은 C 라이브러리 기반의 Event Loop를 사용한다.
라이브러리 언어 사용처 특징 libev C Gevent 경량, Unix 중심 libuv C Node.js, Gevent 크로스플랫폼, 파일 I/O 지원 libgreen C Eventlet Eventlet 전용 4.2 Gevent Event Loop 구조
┌─────────────────────────────────────────────────────────────┐ │ Gevent 아키텍처 │ ├─────────────────────────────────────────────────────────────┤ │ │ │ Python Layer │ │ ───────────── │ │ ┌─────────────────────────────────────────────────────┐ │ │ │ Gevent Hub │ │ │ │ (메인 Event Loop, greenlet으로 구현) │ │ │ │ │ │ │ │ ┌─────────┐ ┌─────────┐ ┌─────────┐ │ │ │ │ │greenlet1│ │greenlet2│ │greenlet3│ ... │ │ │ │ └────┬────┘ └────┬────┘ └────┬────┘ │ │ │ │ │ │ │ │ │ │ │ └────────────┼────────────┘ │ │ │ │ ▼ │ │ │ │ ┌──────────────┐ │ │ │ │ │ Hub.switch()│ │ │ │ │ └──────┬───────┘ │ │ │ └───────────────────┼─────────────────────────────────┘ │ │ │ │ │ C Layer (libev) │ │ │ ─────────────── ▼ │ │ ┌─────────────────────────────────────────────────────┐ │ │ │ libev Event Loop │ │ │ │ │ │ │ │ • ev_io (I/O watcher) │ │ │ │ • ev_timer (Timer) │ │ │ │ • ev_signal (Signal) │ │ │ │ • ev_loop_run() → epoll/kqueue/select │ │ │ └─────────────────────────────────────────────────────┘ │ │ │ └─────────────────────────────────────────────────────────────┘4.3 Monkey Patching
Gevent의 핵심은 Monkey Patching이다.
# Gevent 시작 시 from gevent import monkey monkey.patch_all() # 다음 모듈들이 패치됨: # socket → gevent.socket # ssl → gevent.ssl # time.sleep → gevent.sleep # threading → gevent._threading # 결과: 동기 코드가 자동으로 비동기처럼 동작 import socket s = socket.socket() s.recv(1024) # 블로킹이 아닌 greenlet 전환!┌─────────────────────────────────────────────────────────────┐ │ Monkey Patching 동작 │ ├─────────────────────────────────────────────────────────────┤ │ │ │ Before Patch: │ │ ───────────── │ │ socket.recv() → Blocking I/O (스레드 전체 블록) │ │ │ │ After Patch: │ │ ──────────── │ │ socket.recv() → gevent.socket.recv() │ │ │ │ │ ▼ │ │ ┌───────────────────────────────────────────────────────┐ │ │ │ 1. libev에 I/O watcher 등록 │ │ │ │ 2. hub.switch() → 다른 greenlet으로 전환 │ │ │ │ 3. I/O 완료 시 원래 greenlet으로 복귀 │ │ │ └───────────────────────────────────────────────────────┘ │ │ │ └─────────────────────────────────────────────────────────────┘
5. asyncio vs Gevent Event Loop 비교
5.1 핵심 차이점
특성 asyncio Gevent Event Loop Python 구현 (selectors) C 구현 (libev/libuv) 동시성 단위 Coroutine (async def) Greenlet I/O 처리 명시적 (await) 암시적 (monkey patch) 기존 코드 async/await 필수 동기 코드 그대로 사용 전환 제어 프로그래머가 await로 명시 I/O 시 자동 전환 5.2 구조적 비교
┌─────────────────────────────────────────────────────────────┐ │ Event Loop 구조 비교 │ ├─────────────────────────────────────────────────────────────┤ │ │ │ asyncio: │ │ ───────── │ │ ┌─────────────────────────────────────────────────────┐ │ │ │ asyncio.run() │ │ │ │ │ │ │ │ │ ▼ │ │ │ │ ┌─────────────────┐ │ │ │ │ │ SelectorEventLoop │ ← Python 구현 │ │ │ │ │ │ │ │ │ │ │ Task1 (coro) │ │ │ │ │ │ Task2 (coro) │ ← async/await 필수 │ │ │ │ │ Task3 (coro) │ │ │ │ │ └────────┬──────────┘ │ │ │ │ │ │ │ │ │ ▼ │ │ │ │ epoll/kqueue (OS) │ │ │ └─────────────────────────────────────────────────────┘ │ │ │ │ Gevent: │ │ ──────── │ │ ┌─────────────────────────────────────────────────────┐ │ │ │ gevent.spawn() │ │ │ │ │ │ │ │ │ ▼ │ │ │ │ ┌─────────────────┐ │ │ │ │ │ Gevent Hub │ ← Python 래퍼 │ │ │ │ │ │ │ │ │ │ │ greenlet1 (fn) │ │ │ │ │ │ greenlet2 (fn) │ ← 일반 함수 가능 │ │ │ │ │ greenlet3 (fn) │ │ │ │ │ └────────┬────────┘ │ │ │ │ │ │ │ │ │ ▼ │ │ │ │ ┌─────────────────┐ │ │ │ │ │ libev (C) │ ← C 라이브러리 │ │ │ │ └────────┬────────┘ │ │ │ │ │ │ │ │ │ ▼ │ │ │ │ epoll/kqueue (OS) │ │ │ └─────────────────────────────────────────────────────┘ │ │ │ └─────────────────────────────────────────────────────────────┘
6. Event Loop 충돌 원인
6.1 왜 서로 다른 Event Loop는 충돌하는가?
┌─────────────────────────────────────────────────────────────┐ │ Event Loop 충돌 원인 │ ├─────────────────────────────────────────────────────────────┤ │ │ │ 충돌 시나리오: │ │ ───────────── │ │ │ │ ┌───────────────────────────────────────────────────────┐ │ │ │ Gevent Hub (libev event loop) │ │ │ │ │ │ │ │ │ └── greenlet: my_task() │ │ │ │ │ │ │ │ │ └── asyncio.run(coro) ← ❌ 충돌! │ │ │ │ │ │ │ │ │ └── asyncio event loop 생성 │ │ │ └───────────────────────────────────────────────────────┘ │ │ │ │ 문제점: │ │ ──────── │ │ 1. 두 Event Loop가 동시에 실행 시도 │ │ 2. 각자 I/O를 감시하려 함 (epoll_wait 충돌) │ │ 3. 스레드 제어권 충돌 │ │ │ │ 에러 메시지: │ │ "RuntimeError: Cannot run the event loop while │ │ another loop is running" │ │ │ └─────────────────────────────────────────────────────────────┘6.2 해결 방법
상황 해결책 Gevent 환경에서 async 코드 동기 코드로 변경 (gevent가 자동 처리) asyncio 환경에서 sync 코드 run_in_executor()사용두 Event Loop 혼용 필요 별도 프로세스 분리 # ✅ Gevent 환경에서의 올바른 패턴 # 동기 코드 사용 → gevent가 자동으로 greenlet 전환 def my_task(): response = requests.get("https://api.example.com") # 자동 전환 return response.json() # ❌ Gevent 환경에서의 잘못된 패턴 async def my_async_task(): async with aiohttp.ClientSession() as session: response = await session.get("https://api.example.com") return await response.json() # Gevent 환경에서 asyncio.run(my_async_task()) 호출 시 충돌
7. 이코에코 적용 사례
7.1 Celery Worker Pool과 Event Loop
┌─────────────────────────────────────────────────────────────┐ │ 이코에코 Event Loop 사용 현황 │ ├─────────────────────────────────────────────────────────────┤ │ │ │ API Layer (FastAPI + Uvicorn): │ │ ─────────────────────────────── │ │ • asyncio Event Loop 사용 │ │ • async def 엔드포인트 │ │ • aiohttp, asyncpg 등 async 클라이언트 │ │ │ │ Worker Layer (Celery): │ │ ────────────────────── │ │ • Pool별 다른 Event Loop 사용 │ │ │ │ ┌─────────────────────────────────────────────────────┐ │ │ │ prefork Pool: │ │ │ │ • 프로세스별 독립 asyncio Event Loop 가능 │ │ │ │ • run_async() 헬퍼 사용 가능 │ │ │ └─────────────────────────────────────────────────────┘ │ │ │ │ ┌─────────────────────────────────────────────────────┐ │ │ │ gevent Pool: │ │ │ │ • libev Event Loop (monkey patched) │ │ │ │ • asyncio Event Loop 사용 불가! ← 충돌 │ │ │ │ • 동기 코드만 사용 │ │ │ └─────────────────────────────────────────────────────┘ │ │ │ └─────────────────────────────────────────────────────────────┘7.2 실제 트러블슈팅 사례
# ❌ Before (충돌 발생) @celery_app.task def vision_task(): from domains._shared.celery.async_support import run_async result = run_async(analyze_images_async()) # asyncio loop 실행 시도 return result # ✅ After (정상 동작) @celery_app.task def vision_task(): result = analyze_images() # 동기 함수 (gevent가 자동 처리) return result
8. 핵심 요약
┌─────────────────────────────────────────────────────────────┐ │ 핵심 요약 │ ├─────────────────────────────────────────────────────────────┤ │ │ │ 1. Event Loop = I/O Multiplexing + Callback 실행 │ │ ───────────────────────────────────────────── │ │ • OS의 epoll/kqueue로 I/O 감시 │ │ • Ready 이벤트 발생 시 콜백 실행 │ │ │ │ 2. asyncio vs Gevent │ │ ───────────────────── │ │ • asyncio: Python 구현, async/await 필수 │ │ • Gevent: C 구현 (libev), 동기 코드 자동 변환 │ │ │ │ 3. 충돌 원인 │ │ ─────────── │ │ • 하나의 스레드에 두 Event Loop 공존 불가 │ │ • 각자 I/O 감시 → 제어권 충돌 │ │ │ │ 4. 해결책 │ │ ───────── │ │ • Gevent 환경: 동기 코드 사용 │ │ • asyncio 환경: async/await 사용 │ │ • 혼용 필요: 프로세스 분리 │ │ │ └─────────────────────────────────────────────────────────────┘
References
공식 문서
C 라이브러리
'Knowledge Base > Python' 카테고리의 다른 글
FastAPI Lifespan: 애플리케이션 생명주기 관리 (0) 2026.01.04 FastAPI Clean Example (0) 2025.12.31 arq: AsyncIO-native Task Queue (0) 2025.12.29 동시성 모델과 Green Thread (0) 2025.12.24