[Python] asynchronous 알아가기

dddwsd·2024년 6월 28일
0

비동기 프로그래밍

python은 single thread로 동작하는 인터프리터 언어이기 때문에 동기(synchronous=순차적으로)방식으로 코드를 실행하고 완료가 된 후에 다음 작업을 실행한다.
하지만 대기시간이 많은 I/O작업 같은 경우 sync 방식으로 구현되어 있으면 비효율적이기 때문에 function, method 등이 완료되는 것을 기다리지 않고 다음 작업을 진행하는 비동기(asynchronous) 방식을 사용한다.

1. asyncio

asyncio는 비동기 I/O를 지원하는 python의 내장 라이브러리로 coroutine과 task를 사용하여 비동기 프로그래밍을 지원한다.
asyncio는 event loop을 기반으로 비동기 함수(async def)와 비동기 작업을 관리한다.

1-1. coroutine

coroutine은 비동기 함수의 일종으로, 일시 중단되었다가 나중에 다시 시작될 수 있는 함수로 async 를 붙여서 정의한다.
await 를 사용하여 다른 coroutine을 호출하거나 비동기 작업을 기다릴 수 있다.

아래 예시의 경우 say_after(1, 'hello')라는 coroutine이 실행 된 후에 say_after(2, 'world')라는 coroutine이 실행되기 때문에 총 3초가 걸린다.

import asyncio
import time

async def say_after(delay, what):
    await asyncio.sleep(delay)
    print(what)

async def main():
    print(f"started at {time.strftime('%X')}")

    await say_after(1, 'hello')
    await say_after(2, 'world')

    print(f"finished at {time.strftime('%X')}")

asyncio.run(main())

출력

started at 17:13:52
hello
world
finished at 17:13:55

1-2. task

task는 coroutine을 실행하기 위해 event loop에 의해 관리되는 객체로 생성되자마자 비동기적으로 실행된다.
task는 coroutine을 wrapping 하여 실행 상태를 관리하고, 실행 결과를 추적할 수 있다.

아래 예시의 경우 task1을 생성하고 task2를 생성한 후에 await을 통해 비동기적으로 실행된 task의 결과를 기다리기 때문에 총 2초가 걸린다.

async def main():
    task1 = asyncio.create_task(
        say_after(1, 'hello'))

    task2 = asyncio.create_task(
        say_after(2, 'world'))

    print(f"started at {time.strftime('%X')}")

    # Wait until both tasks are completed (should take
    # around 2 seconds.)
    await task1
    await task2

    print(f"finished at {time.strftime('%X')}")

예상 출력

started at 17:14:32
hello
world
finished at 17:14:34

1-3. coroutine task 차이점

coroutinetask
정의async def로 정의된 함수asyncio.create_task로 생성된 coroutine의 wrapper
실행방식await를 통해서 호출됨으로써 실행생성되자마자 event loop에 의해 스케줄링되어 병렬로 실행
목적비동기 작업을 정의하기 위한 기본 단위coroutine을 실행 관리하여 비동기 작업을 동시에 수행할 수 있게 함
상태관리상태 관리하지 않음실행 상태, 결과 및 예외 추적

2. uvloop

uvloop은 node.js에서 사용되는 고성능 비동기 I/O 라이브러리인 libuv를 기반으로 구현된 파이썬용 이벤트 루프로 libuv를 사용하여 파이썬의 event loop를 대체함으로써 asyncio보다 훨씬 빠른 성능을 제공한다.
많은 벤치마크 테스트에서 uvloopasyncio 이벤트 루프에 비해 2배에서 4배 정도 빠른 성능을 보였다.

uvloop을 사용하기 위해서는 event loop정책을 uvloop로 설정하면 된다.

import asyncio
import uvloop

asyncio.set_event_loop_policy(uvloop.EventLoopPolicy())

async def main():
    print("Hello, World!")

asyncio.run(main())

하지만 파이썬 표준 라이브러리인 asyncio와 다르게 uvloop는 서드파티 라이브러리라서 python 버전이나 특정 라이브러리와의 dependency 문제가 발생할 수 있으며 도입전에 많은 테스트를 거쳐야 한다.

python web api 구현에 자주 사용되는 fastapi의 경우 v0.111.0부터 uvloop을 dependency library로 설치해서 사용하고 있다.

3. nest_asyncio

기본적으로 asynciouvloop의 경우 중첩된 event loop의 실행을 허용하지 않는데, nest_asyncio는 기존의 event loop가 실행중인 상황에서도 새로운 event loop를 중첩하여 실행할 수 있도록 한다.
주로 jupyter notebook나 fastapi와 같은 비동기 프레임워크에서 사용중인 event loop와 개발자가 구현한 event loop 간의 충돌을 피하기 위해 사용되며 성능 향상을 목적으로 하지는 않는다.

nest_asyncio를 사용하기 위해서는 nest_asyncio.apply()를 호출하기만 하면 된다.

import nest_asyncio
import asyncio

nest_asyncio.apply()

async def main():
    print("Hello, World!")

asyncio.run(main())

Refer

profile
Github - https://github.com/dddwsd

0개의 댓글