본문 바로가기
C.W.K.
Stream
Lesson 01 of 10 · published

왜 Async — Event Loop 문제

~12 min · async, event-loop, asyncio

Level 0Scout
0 XP0/80 lessons0/10 achievements
0/120 XP to next level120 XP to go0% complete

Blocking 호출 하나가 협력 스케줄링을 죽여

Python asyncio는 기본적으로 모든 걸 thread 하나 위에서 돌려. task 하나가 I/O를 기다리면 — DB를 읽든 HTTP를 쏘든 — event loop가 그동안 다른 task를 굴려. 이게 전부의 전제야.

함정은 여기 있어. asyncioawait가 걸린 자리에서만 '기다리는 중'이라는 걸 알아채. 디스크에서 200ms 동안 붙들려 있는 sync 호출은 그 200ms 내내 event loop를 통째로 얼려. 다른 모든 request와 task와 websocket frame까지 같이.

그런데 평범한 sqlite3.connect(...)conn.execute(...)가 바로 그 sync C 호출이야. async 웹 앱 안에서는 DB query 하나하나가 event loop를 멈춰 세우는 셈이지. 유저가 한 명이면 티도 안 나. 동시에 100명이면 무너져.

해법은 둘이야.

  • aiosqlite — SQLite를 백그라운드 thread에서 돌리고 executefetchone 같은 걸 async로 열어줘. await하는 동안 DB 호출은 그 thread에서 돌고 event loop는 다른 일을 해.
  • asyncio.to_thread — sync 호출을 손으로 떼어 넘기는 방식이야. 되긴 하는데, aiosqlite가 이미 해둔 connection 관리를 다시 만들게 돼.
Self-reference: 피파 backend(backend/store/conversations.py)가 aiosqlite를 쓰는 게 정확히 이 이유야. Claude가 SSE로 답을 흘리는 동안에도 conversation을 저장하면서 event loop를 계속 양보해야 하거든. sync로 썼다면 UI에서 눈에 보이게 끊겼을 거야.

Code

Async loop에서 blocking이 무슨 짓을 하나·python
import asyncio, sqlite3, time

async def heartbeat():
    while True:
        print('tick', time.strftime('%H:%M:%S'))
        await asyncio.sleep(0.1)

async def slow_sync_query():
    conn = sqlite3.connect('big.db')
    conn.execute('SELECT count(*) FROM huge_table').fetchone()  # event loop block

async def main():
    heart = asyncio.create_task(heartbeat())
    await slow_sync_query()    # 호출 동안 heartbeat freeze
    heart.cancel()

asyncio.run(main())

External links

Exercise

얼어붙는 걸 직접 재현해봐. 작은 FastAPI나 aiohttp 서버에 endpoint를 둘 두는 거야. 하나는 느린 sync SQLite query를 돌리고, 다른 하나는 즉시 async로 답해. 느린 쪽에 동시 요청을 퍼붓고 빠른 쪽의 지연이 어떻게 망가지는지 재봐. 그다음 느린 endpoint를 aiosqlite로 갈아끼우고 다시 재.

Progress

Progress is local-only — sign in to sync across devices.
이 페이지에서 버그를 발견하셨거나 피드백이 있으세요?문제 신고

댓글 0

🔔 답글 알림 (로그인 필요)
로그인댓글을 남기려면 로그인해 주세요.

아직 댓글이 없어요. 첫 댓글을 남겨보세요.