클라-서버간의 통신 비동기 말고 서버 성능을 위한 비동기인 파이썬의 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
코드 해석:
- worker 코루틴은 asyncio.sleep() 써서 지정된 시간만큼 비동기 대기.
- main 코루틴은 asyncio.create_task() 써서 worker 태스크 두 개 생성.
- await task1, await task2는 태스크 완료될 때까지 기다림.
- 이벤트 루프는 worker 코루틴이 await asyncio.sleep() 만나면, 다른 태스크 실행.
- 결과적으로, 두 태스크는 동시에 실행되는 것처럼 보이고, 총 실행 시간은 가장 오래 걸리는 태스크 실행 시간이랑 비슷. (약 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 효과 별로. 이런 작업은 병렬처리 쓰는 게 더 좋다.
'Python' 카테고리의 다른 글
Python Ray 라이브러리 (병렬처리) (0) | 2025.01.07 |
---|---|
파이썬 복습 (0) | 2023.11.11 |