Summary
- 비동기는 I/O 대기 시간 동안 다른 작업을 처리해 동시성(Concurrency)을 극대화하는 기법이다
- I/O Bound 문제에 효과적이며, CPU Bound는 멀티프로세싱이 더 적합하다
async def로 코루틴을 정의하고await로 제어권을 이벤트 루프에 양보한다gather(동시 실행),Semaphore(동시 개수 제한),Lock(경쟁 상태 방지)이 핵심 도구다
병목의 원인
I/O Bound vs CPU Bound
비동기 프로그래밍의 목적은 I/O Bound 문제를 해결하는 것이다.
- I/O Bound: 실행 시간 대부분을 대기에 쓰는 경우. 웹 다운로드, DB 쿼리, 파일 읽기/쓰기, API 요청. → 비동기(asyncio)가 효과적
- CPU Bound: CPU가 쉬지 않고 계산하는 경우. 머신러닝 학습, 동영상 인코딩. → 멀티프로세싱이 적합
동시성 vs 병렬성
- 동시성(Concurrency): 싱글 코어에서 여러 작업이 번갈아 실행되어 동시에 실행되는 것처럼 보이는 논리적 개념. asyncio가 구현하는 방식이다
- 병렬성(Parallelism): 멀티 코어에서 물리적으로 여러 작업이 실제로 동시에 실행(multiprocessing의 영역)
블로킹 vs 논블로킹
- 블로킹 I/O: 특정 작업이 완료될 때까지 제어권을 반환하지 않는다. 호출자는 작업이 끝날 때까지 아무것도 못 한다
- 논블로킹 I/O: 호출된 함수가 작업을 즉시 시작하고 제어권을 바로 반환한다. 완료되면 콜백·이벤트로 결과를 통지받는다
비동기 프로그래밍이란
비동기 프로그래밍은 특정 작업의 완료를 기다리지 않고 다음 작업을 수행하는 실행 모델이다. 요청(Request)과 결과 처리(Response)를 분리해, 대기 시간 동안 CPU가 유휴 상태에 빠지는 것을 막고 동시성을 극대화한다.
라면 끓이기에 비유하면, 동기는 물이 끓을 때까지 앞에서 멍하니 기다리는 것이고, 비동기는 물이 끓기를 기다리는 동안(await) 숙제를 하다가 끓었다는 알림이 오면 면을 넣는 것이다.
asyncio 핵심 문법
async def와 await
async def는 코루틴(Coroutine)을 정의하는 키워드로, 호출해도 즉시 실행되지 않고 실행 가능한 코루틴 객체를 반환한다. await는 awaitable 객체 뒤에 쓰며, 현재 코루틴을 일시 중지하고 제어권을 이벤트 루프에 넘겨 다른 작업이 실행되게 한다.
- 이벤트 루프(Event Loop): 무한 루프를 돌며 등록된 작업의 상태를 감시하고, 완료된 태스크를 깨워 실행을 재개시키는 비동기의 심장부
- 코루틴(Coroutine): 실행 중간에 상태를 유지한 채 일시 정지하고 제어권을 양보한 뒤 나중에 재개할 수 있는 함수
주요 메서드
| 메서드 | 핵심 기능 |
|---|---|
run | 비동기 프로그램의 진입점. 이벤트 루프를 생성·실행 |
get_running_loop | 현재 실행 중인 이벤트 루프 객체 반환 |
gather | 여러 코루틴을 동시에 실행하고 결과를 입력 순서대로 취합 |
as_completed | 여러 코루틴을 먼저 완료되는 순서대로 반환(진행률 표시에 유용) |
run_in_executor | 동기(Blocking) 함수를 별도 스레드에서 실행해 루프 차단 방지 |
Semaphore | 동시 실행 가능한 작업의 최대 개수 제한 |
Lock | 공유 자원에 한 번에 하나의 작업만 접근하도록 강제 |
run — 진입점
새 이벤트 루프를 생성하고 코루틴을 완료될 때까지 실행한 뒤 정리한다. 보통 if __name__ == "__main__":에서 한 번 호출한다.
import asyncio
async def main():
print("Hello")
await asyncio.sleep(1)
print("World")
if __name__ == "__main__":
asyncio.run(main())gather — 동시 실행 후 순서대로 취합
모든 코루틴을 태스크로 감싸 이벤트 루프에 등록하고, 완료되면 결과를 입력 순서대로 리스트에 담아 반환한다.
async def fetch_data(name, seconds):
await asyncio.sleep(seconds)
return f"{name} 데이터"
async def main():
# 총 소요 시간은 가장 긴 3초. 결과는 입력 순서(A, B, C)대로
results = await asyncio.gather(
fetch_data("A", 3),
fetch_data("B", 1),
fetch_data("C", 2)
)
print(results) # ['A 데이터', 'B 데이터', 'C 데이터']as_completed — 완료 순서대로
전체가 끝나길 기다리지 않고 먼저 끝난 작업부터 후처리할 때 쓴다.
async def main():
tasks = [download_image("이미지1", 3),
download_image("이미지2", 1),
download_image("이미지3", 2)]
# 완료 순서대로 반환 (2 → 3 → 1)
for completed_task in asyncio.as_completed(tasks):
result = await completed_task
print(result)run_in_executor — 동기 함수 통합
asyncio는 단일 스레드이므로 time.sleep이나 파일 I/O 같은 동기 함수를 호출하면 루프 전체가 멈춘다. 이 메서드는 별도 스레드 풀에서 동기 함수를 실행하도록 위임한다.
def blocking_io():
time.sleep(2) # executor를 쓰면 전체를 멈추지 않음
async def main():
loop = asyncio.get_running_loop()
await loop.run_in_executor(None, blocking_io)Semaphore — 동시 실행 수 제한
async with semaphore: 블록 진입 시 카운터가 감소하고, 0이면 다른 작업은 대기한다. API 서버에 과도한 트래픽을 보내지 않도록 동시 접속 수를 제어한다.
async def access_resource(sem, i):
async with sem:
print(f"작업 {i} 진입")
await asyncio.sleep(2)
async def main():
sem = asyncio.Semaphore(2) # 동시에 최대 2개
await asyncio.gather(*[access_resource(sem, i) for i in range(5)])Lock — 경쟁 상태 방지
특정 자원에 한 번에 하나의 코루틴만 접근하도록 강제한다. 여러 작업이 동시에 하나의 파일에 쓰기를 시도할 때 데이터가 깨지는 경쟁 상태(Race Condition)를 방지한다.
lock = asyncio.Lock()
async def safe_increment():
global shared_resource
async with lock: # 락을 획득해야 실행 가능
temp = shared_resource
await asyncio.sleep(0.1)
shared_resource = temp + 1Reference