본문 바로가기
Python

파이썬 비동기 프로그래밍 asyncio

by shulk 2025. 1. 8.

클라-서버간의 통신 비동기 말고 서버 성능을 위한 비동기인 파이썬의 asyncio 라이브러리를 사용해서 비동기 프로그래밍을 하는 방법에 대해 정리해 보겠다

asyncio는 단일 스레드에서 여러 작업을 동시에 처리하는 것처럼 보이게 해서, I/O 바운드 작업의 성능을 크게 향상시킬 수 있는 도구다.

동기 vs. 비동기 예시

동기식 프로그래밍은 한 명의 요리사한 번에 하나의 요리만 하는 레스토랑. 손님이 스테이크 주문하면, 요리사는 스테이크 다 구워질 때까지 아무것도 안 하고 멀뚱멀뚱. 다음 손님 파스타는 스테이크 다 만든 다음에야 시작.

비동기식 프로그래밍은 한 명의 짱짱 요리사여러 요리를 동시에 하는 레스토랑. 스테이크 굽기 시작하고, 기다리지 않고 바로 파스타 면 삶기 시작. 면 삶는 동안 샐러드도 만들고. 이렇게 여러 작업 왔다 갔다 하면서 처리하니, 전체 요리 시간 훨씬 단축!

파이썬 asyncio 핵심 개념

asyncio 쓰려면 몇 가지 핵심 개념을 알아야 한다.

  • 이벤트 루프 (Event Loop): 발생하는 이벤트(네트워크 요청 완료, 타이머 만료 등) 감지하고, 그에 맞는 작업 실행하는 지휘자. 레스토랑 매니저 같은 역할.
  • 코루틴 (Coroutine): async def로 정의된 함수. await 써서 다른 코루틴 실행 기다릴 수 있음. 레스토랑 요리사.
  • 퓨처 (Future): 비동기 작업 결과를 나타내는 객체. 작업 상태(대기, 완료, 취소)랑 결과 저장. 레스토랑 주문서.
  • 태스크 (Task): 코루틴을 감싸서 실행 가능한 작업 단위로 만든 것. asyncio.create_task()로 생성. 레스토랑에서 요리사한테 전달된 주문서.

asyncio 사용한 비동기 프로그래밍 예제

import asyncio
import time

async def worker(name, delay):
    print(f"{name} started")
    await asyncio.sleep(delay) # delay 초 동안 대기 (비동기 대기)
    print(f"{name} finished after {delay} seconds")
    return f"{name} result"

async def main():
    start_time = time.time()

    # 태스크 생성
    task1 = asyncio.create_task(worker("Task 1", 2))
    task2 = asyncio.create_task(worker("Task 2", 3))

    # 태스크 실행 및 결과 기다림
    result1 = await task1
    result2 = await task2

    end_time = time.time()
    print(f"Total time: {end_time - start_time:.2f} seconds")
    print(f"Result 1: {result1}")
    print(f"Result 2: {result2}")

if __name__ == "__main__":
    asyncio.run(main())


######################출력 결과##########################
Task 1 started
Task 2 started
Task 1 finished after 2 seconds
Task 2 finished after 3 seconds
Total time: 3.00 seconds
Result 1: Task 1 result
Result 2: Task 2 result

코드 해석:

  1. worker 코루틴은 asyncio.sleep() 써서 지정된 시간만큼 비동기 대기.
  2. main 코루틴은 asyncio.create_task() 써서 worker 태스크 두 개 생성.
  3. await task1, await task2는 태스크 완료될 때까지 기다림.
  4. 이벤트 루프는 worker 코루틴이 await asyncio.sleep() 만나면, 다른 태스크 실행.
  5. 결과적으로, 두 태스크는 동시에 실행되는 것처럼 보이고, 총 실행 시간은 가장 오래 걸리는 태스크 실행 시간이랑 비슷. (약 3초)

asyncio.wait() vs. asyncio.gather()

여러 태스크 동시 실행하고 결과 기다리려면 asyncio.wait(), asyncio.gather() 사용.

  • asyncio.wait(): 여러 태스크 중 하나라도 완료될 때까지 기다림. return_when 인자로 완료 조건 설정 가능. (FIRST_COMPLETED, FIRST_EXCEPTION, ALL_COMPLETED)
  • asyncio.gather(): 여러 태스크 모두 완료될 때까지 기다림. 각 태스크 결과 리스트로 반환.
import asyncio

async def my_task(id, delay):
    print(f"Task {id} starting")
    await asyncio.sleep(delay)
    print(f"Task {id} finishing")
    return id

async def main():
    task1 = asyncio.create_task(my_task(1, 2))
    task2 = asyncio.create_task(my_task(2, 1))
    task3 = asyncio.create_task(my_task(3, 3))

    # asyncio.wait() 사용 (FIRST_COMPLETED)
    done, pending = await asyncio.wait([task1, task2, task3], return_when=asyncio.FIRST_COMPLETED)
    print("Done tasks (wait):", [task.result() for task in done])

    # 남은 task 취소
    for task in pending:
        task.cancel()
    
    # asyncio.gather() 사용
    results = await asyncio.gather(
        asyncio.create_task(my_task(4, 1)),
        asyncio.create_task(my_task(5, 2))
    )
    print("Gather results:", results)

if __name__ == "__main__":
    asyncio.run(main())

#####################출력###########################
Task 1 starting
Task 2 starting
Task 3 starting
Task 2 finishing
Done tasks (wait): [2]
Task 4 starting
Task 5 starting
Task 4 finishing
Task 5 finishing
Gather results: [4, 5]

언제 asyncio를 써야 할까?

asyncio는 I/O 바운드 작업 많이 하는. 네트워크 요청, 파일 읽기/쓰기, DB 쿼리 등 외부 리소스 응답 기다리는 시간 많은 작업에 쓰면 효율적.

반대로, CPU 바운드 작업(복잡한 계산, 이미지 처리 등)에는 asyncio 효과 별로. 이런 작업은 병렬처리 쓰는 게 더 좋다.

 

다른 참고 블로그1, 블로그2

'Python' 카테고리의 다른 글

Python Ray 라이브러리 (병렬처리)  (0) 2025.01.07
파이썬 복습  (0) 2023.11.11