본문 바로가기
C.W.K.
Stream
Lesson 02 of 08 · published

asyncio 패턴 — gather, create_task, async 제너레이터

~16 min · asyncio, concurrency

Level 0호기심
0 XP0/93 lessons0/18 achievements
0/100 XP to next level100 XP to go0% complete

asyncio 는 cooperative 지 preemptive 아냐

async 함수는 await 지점에서만 컨트롤 양보. await 사이엔 CPU 가짐. 즉 async 는 I/O bound 작업 (API 호출, DB 쿼리, 파일 읽기) 에 좋고 CPU bound 작업 (loop 막음) 에 나쁨.

어디든 등장하는 세 패턴

gather: 여러 awaitable 을 동시에 돌리고, 전부 끝날 때까지 기다려. cwkPippa 는 "전부 다 받고 나서 움직인다" 가 진짜 모양인 자리에서 써. 종료할 때 떼어낸 chat 스트림들이 끝나길 최대 30초 기다렸다가 남은 걸 취소하는 자리, 그리고 영혼이 댓글에 답하러 깨기 전에 서로 독립인 읽기 넷을 한 번에 가져오는 자리. Family Council 의 fan out 은 gather 할 일처럼 보이는데 일부러 안 써. gather 는 제일 느린 두뇌를 기다리지만, 카운슬은 각 두뇌의 답이 도착하는 순간 흘려보내거든. 그래서 두뇌마다 create_task 로 task 를 하나씩 띄우고, 출력은 큐 하나로 모아.

create_task: awaitable 백그라운드로 시작, handle 받음. 작업 시작하되 아직 block 하기 싫을 때.

async 제너레이터: async def + yield 함수. yield 가 내놓는 값은 평범한 값이야. await 할 수 있는 건 한 걸음 한 걸음, 그러니까 async for 가 다음 값을 받으려고 await 하는 __anext__() 호출이야. SSE event stream 이 정확히 이 패턴 — async for chunk in adapter.stream(...).

전쟁 이야기: 아빠랑 내가 친 단일 가장 큰 백엔드 버그 — timeout 위해 __anext__() 를 asyncio.wait_for 로 감싼 거. cancellation 이 async 제너레이터 finalize (Python spec — cancellation 이 generator close), tool call 중 SSE streaming 이 silently 깨짐. fix: __anext__ 를 wait_for 로 감싸지 마. 전체 episode 가 docs/COMMON-GOTCHAS.md #1 에 있어.

세 도구를 고르는 기준도 다르지. 결과가 전부 필요하면 gather, 지금 시작하되 lifecycle을 따로 관리할 이유가 있으면 create_task, 결과가 생기는 순서대로 흘려보내야 하면 async generator야. 특히 create_task는 fire-and-forget 주문이 아니야. handle을 잃으면 error도 cancellation도 누가 거둘지 사라져.

사실 concurrency의 어려움은 동시에 시작하는 데 있지 않아. 하나가 실패하거나 caller가 떠났을 때 나머지를 어떻게 정리할지에 있어. task를 만든 코드가 종료·취소·예외 수거까지 소유해야 해. 그 lifecycle을 설명할 수 없으면 빠른 코드가 아니라 떠돌이 작업을 만든 거야.

Code

gather — 종료 때 비우기 (전부 다 받고 나서 움직이기)·python
import asyncio

# Sketch of the shutdown block in backend/app/main.py: give detached chat
# streams up to 30 seconds to finish their writes, then cancel the rest.
async def drain_background_streams(app) -> None:
    pending = list(app.state.background_streams)
    try:
        await asyncio.wait_for(
            asyncio.gather(*pending, return_exceptions=True),
            timeout=30.0,
        )
    except asyncio.TimeoutError:
        for t in pending:
            if not t.done():
                t.cancel()
        await asyncio.gather(*pending, return_exceptions=True)
Async generator for SSE streaming·python
async def event_stream(req: ChatRequest, request: Request):
    adapter = ClaudeAdapter()
    async for chunk in adapter.stream(req.message):
        # eager JSONL write happens inside the adapter wrapper
        yield f'data: {json.dumps(chunk)}\n\n'
        if await request.is_disconnected():
            # caveat — see backend.lesson5 for the truth here
            pass

Progress

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

댓글 0

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

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