여기서는 문법을 외우기보다 이 문제가 I/O 대기 중심인지, 그리고 함께 기다릴지 먼저 시작만 해 둘지를 먼저 봅니다.
asyncio는 CPU 계산을 마법처럼 빠르게 만드는 기능이 아니라, 대기 시간이 많은 작업을 이벤트 루프로 겹쳐 처리하는 모델입니다.
빠른 흐름
import asyncio
async def fetch_data(name: str) -> str:
await asyncio.sleep(0.5)
return f"data:{name}"
async def main() -> None:
results = await asyncio.gather(fetch_data("a"), fetch_data("b"))
print(results)
asyncio.run(main())기본 흐름
기본형은 coroutine 정의, await, 최상위 실행, 함께 기다리기, 먼저 시작하기를 같이 보면 됩니다.
import asyncio
async def work():
return 1
async def main():
value = await work()
results = await asyncio.gather(work(), work())
task = asyncio.create_task(work())
result = await task
asyncio.run(main())async def는 호출 즉시 끝까지 실행되는 함수가 아니라 coroutine 객체를 만듭니다.
실제 실행은 이벤트 루프가 맡고, await는 그 진행을 조율하는 지점입니다.
coroutine 모델
async def를 호출하면 coroutine 객체가 생긴다
async def 함수는 호출 즉시 끝까지 실행되지 않습니다.
우선 coroutine 객체를 만들고, 실제 실행은 이벤트 루프가 담당합니다.
async def work():
return 1
coro = work()
print(coro)이 점 때문에 async 코드는 "지금 바로 실행"보다 "스케줄링해서 진행" 감각으로 읽는 편이 맞습니다.
await는 현재 coroutine이 제어권을 잠시 넘기는 지점이다
await는 프로그램 전체를 멈추는 문법이 아니라, 현재 coroutine이 다른 작업이 돌 수 있게 제어권을 넘기는 지점입니다.
async def fetch():
await asyncio.sleep(1)
return "done"그래서 async는 기다리는 시간이 많은 문제에 잘 맞고, CPU 계산이 많은 작업을 자동으로 해결해 주지는 않습니다.
동시 대기
여러 결과를 같이 기다리면 gather()를 쓴다
여러 awaitable을 동시에 진행하고 최종 결과를 함께 받고 싶다면 asyncio.gather()가 가장 직접적입니다.
results = await asyncio.gather(fetch_data("a"), fetch_data("b"))즉 "이 작업들을 묶어서 같이 끝낼까?"라는 질문에는 gather()를 먼저 떠올리면 됩니다.
작업 시작
먼저 시작해 두고 나중에 합류하면 create_task()가 맞다
백그라운드로 작업을 시작해 두고, 중간에 다른 일을 하다가 나중에 합류하고 싶다면 create_task()가 자연스럽습니다.
task = asyncio.create_task(fetch_data("c"))
print("do other work")
result = await task즉 gather()는 함께 기다리는 패턴, create_task()는 먼저 시작해 두는 패턴으로 나누면 헷갈림이 줄어듭니다.
results = await asyncio.gather(fetch_data("a"), fetch_data("b"))
task = asyncio.create_task(fetch_data("c"))
await do_other_work()
result = await task둘 다 동시에 시작할 수는 있지만, gather()는 "같이 묶어 기다리기", create_task()는 "지금 시작만 해 두고 합류하기"에 더 가깝습니다.
실행 경계
최상위에서는 보통 asyncio.run()으로 시작한다
스크립트나 프로그램의 시작점에서 async 코드를 실행할 때는 보통 asyncio.run(main())으로 이벤트 루프를 엽니다.
다만 프레임워크나 REPL 환경에서는 이미 루프를 관리할 수 있으므로 실행 환경을 같이 봐야 합니다.
주의할 점
coroutine 객체를 만들기만 하고 await하지 않으면 실제로 실행되지 않을 수 있습니다.
# ❌ coroutine만 만들고 끝냄
async def fetch():
await asyncio.sleep(1)
return "done"
fetch()
# ✅ await하거나 task로 스케줄링
result = await fetch()
# 또는
asyncio.create_task(fetch())
# ❌ CPU 계산이 많다고 async만 붙인다고 빨라지지 않음
async def crunch():
return sum(i * i for i in range(10_000_000))참고 링크
2 sources