본문 바로가기
Python/Study

[Python] asyncio ─ 비동기 방식

by VANCODER 2024. 11. 18.

asyncio

: `async/await` 구문을 사용하여 동시성 코드를 작성할 수 있게 해주는 라이브러리

  • 단일 스레드 작업을 병렬로 처리
  • 고성능 네트워크 및 웹 서버, 데이터베이스 연결 라이브러리, 분산 작업 큐 등을 제공하는 여러 파이썬 비동기 프레임워크의 기반으로 사용
  • 종종 IO 병목이면서 고수준의 구조화된 네트워크 코드에 가장 적합
파이썬 3.7 버전 이상부터 사용 가능

 

 

고수준 API

  1. 파이썬 코루틴들을 동시에 실행하고 실행을 완전히 제어할 수 있음
  2. 네트워크 IO와 IPC를 수행
  3. 자식 프로세스를 제어
  4. 를 통해 작업을 분산
  5. 동시성 코드를 동기화

저수준 API

  1. 네트워킹 , 하위 프로세스 실행 , OS 신호 처리 등을 위한 비동기 API를 제공하는 이벤트 루프 생성, 관리
  2. 트랜스포트를 사용하여 효율적인 프로토콜을 구현
  3. 콜백 기반 라이브러리와 async/await 구문을 사용한 코드 간에 다리를 놓음

예제

e.g. 서로 다른 입력 값으로 `sum()` 함수를 2번 수행하여 결괏값을 출력

import time


def sleep():
    time.sleep(1)


def sum(name, numbers):
    start = time.time()
    total = 0
    for number in numbers:
        sleep()
        total += number
        print(f'작업중={name}, number={number}, total={total}')
    end = time.time()
    print(f'작업명={name}, 걸린시간={end-start}')
    return total


def main():
    start = time.time()

    result1 = sum("A", [1, 2])
    result2 = sum("B", [1, 2, 3])

    end = time.time()
    print(f'총합={result1+result2}, 총시간={end-start}')


if __name__ == "__main__":
    main()

 

`sum()` 함수에서 입력 값을 하나씩 더할 때마다 `sleep()` 함수에 의해 1초씩 시간 소요

 

실행 결과

작업중=A, number=1, total=1
작업중=A, number=2, total=3
작업명=A, 걸린시간=2.000162124633789
작업중=B, number=1, total=1
작업중=B, number=2, total=3
작업중=B, number=3, total=6
작업명=B, 걸린시간=3.0002427101135254
총합=9, 총시간=5.0004048347473145

 

총 5초의 시간이 걸리고, 총합은 9

 

→ `sum()` 함수를 비동기 방식으로 호출하면 실행 시간을 줄일 수 있을 것

 

asyncio 모듈 적용

import asyncio
import time


async def sleep():
    await asyncio.sleep(1)


async def sum(name, numbers):
    start = time.time()
    total = 0
    for number in numbers:
        await sleep()
        total += number
        print(f'작업중={name}, number={number}, total={total}')
    end = time.time()
    print(f'작업명={name}, 걸린시간={end-start}')
    return total


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

    task1 = asyncio.create_task(sum("A", [1, 2]))
    task2 = asyncio.create_task(sum("B", [1, 2, 3]))

    await task1
    await task2

    result1 = task1.result()
    result2 = task2.result()

    end = time.time()
    print(f'총합={result1+result2}, 총시간={end-start}')


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

 

실행 결과

c:\projects\pylib>python asyncio_sample.py
작업중=A, number=1, total=1
작업중=B, number=1, total=1
작업중=A, number=2, total=3
작업명=A, 걸린시간=2.000617742538452
작업중=B, number=2, total=3
작업중=B, number=3, total=6
작업명=B, 걸린시간=3.000927209854126
총합=9, 총시간=3.000927209854126

 

A 작업과 B 작업을 교대로 호출 (제어권이 `await`에 의해 계속 바뀜)

시간도 5초 걸리던 것이 3초만 걸리게 되므로

A, B 작업이 완전히 비동기적으로 동작했다는 것을 알 수 있음

 

 

1) 코루틴

함수를 비동기로 호출하려면 `def` 앞에 `async` 키워드를 넣으면 됨

→ 비동기 함수가 됨

*코루틴: `async`를 적용한 비동기 함수

 

 

2) await

코루틴 안에서 다른 코루틴을 호출할 때, `await sleep()`과 같이 `await`를 함수명 앞에 붙여 호출해야 함

*넌블록킹(non-blocking): 코루틴 수행 중 `await 코루틴`을 만나면 `await`로 호출한 코루틴이 종료될 때까지 기다리지 않고, 제어권을 메인 스레드나 다른 코루틴으로 넘김

 

그리고 호출한 코루틴이 종료되면 이벤트에 의해 다시 그 이후 작업 수행

 

 

3) asyncio.sleep(1)

`sleep()` 함수에서 `time.sleep(1)` 대신 `asyncio.sleep(1)`를 사용
... 코루틴이 아닌 `time.sleep(1)`을 사용한다면 `await`가 적용되지 않아 실행 시간을 줄일 수 없음

 

 

4) asyncio.create_task()

`asyncio.create_task()`: 수행할 코루틴 작업(태스크) 생성
→ 작업을 생성할 뿐, 실제로 코루틴이 수행되는 것은 아님

 

실제 코루틴 실행은 `await 태스크`가 담당

실행 태스크의 결괏값은 `태스크.result()`로 얻을 수 있음

 

⭐ `asyncio.create_task()`는 코루틴을 동시에 실행하는 데 꼭 필요

다음처럼 태스크가 아닌 `await`로 코루틴을 실행한다면

코루틴이 동시에 실행되지 않고, 하나씩 차례로 실행되어 이득이 없을 것

result1 = await sum("A", [1, 2])
result2 = await sum("B", [1, 2, 3])

 

 

5) asyncio.run(main())

`asyncio.run(main())`: 런 루프를 생성하여 `main()` 코루틴을 실행
  • 코루틴을 실행하려면 런 루프가 반드시 필요
  • 코루틴이 모두 비동기적으로 실행되기 때문에 그 시작과 종료를 감지할 수 있는 이벤트 루프가 반드시 필요하기 때문

참고 문서

https://wikidocs.net/125092

https://docs.python.org/ko/3/library/asyncio.html

https://docs.python.org/ko/3/library/asyncio-task.html