본문 바로가기

Python

Python3.8 asyncio, async/await 기초 - 코루틴과 태스크

 

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

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

 

코루틴과 태스크 — Python 3.8.0 문서

코루틴과 태스크 이 절에서는 코루틴과 태스크로 작업하기 위한 고급 asyncio API에 관해 설명합니다. async/await 문법으로 선언된 코루틴은 asyncio 응용 프로그램을 작성하는 기본 방법입니다. 예를 들어, 다음 코드 조각(파이썬 3.7 이상 필요)은 "hello"를 인쇄하고, 1초 동안 기다린 다음, "world"를 인쇄합니다: >>> import asyncio >>> async def main(): ... print('hello') .

docs.python.org

파이썬 공식 문서를 보는 것이 제일 정확하지만, 제가 헷갈렸던 부분을 정리했습니다.

* 공식 문서도 같이 봐주세요.

 

1. 왜 asyncio를 사용해야 하는가

 Threading을 사용한 동시성 제어는 느립니다. Global interpreter lock 때문에, 실제로 여러 물리 쓰레드에서 실행되는 것이 아닌 메인쓰레드에서 모든 연산을 처리하기 때문입니다. 그러면서도 동시에 멀티쓰레드 프로그래밍에서 신경써야 할 문제들을 그대로 가지고 있습니다. (높은 컨텍스트 스위칭 비용, 레이스 컨디션, 데드락 등) 우리가 Threading을 사용해서 얻는 이점은 연산시간이 아닌 파일 읽고 쓰기, Http 통신 대기와 같은 Blocking IO 대기시간입니다.

 그렇다면 우리는 Blocking IO만 신경쓰면 될텐데, 쓰레딩은 너무나 많은 지식들을 요구합니다. 그래서 asyncio 가 나온 것입니다. asyncio 는 쉽고, 명시적으로 흐름을 알 수 있으며 멀티쓰레드 프로그래밍에서 신경써야 할 문제들을 무시할 수 있습니다.

 

2. 코루틴과 태스크의 차이

공식 문서에서 코드만 보고서는 약간 이해하기 힘든 부분입니다.

아래는 대략적인 사용법을 담고있는 공식문서 코드입니다. 

코루틴 

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

 

태스크

import asyncio
import time

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

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

눈치있는 프로그래머라면 함수앞에 async를 붙여 비동기 함수로 선언하고, 비동기 함수 내부에서 await로 비동기 작업을 대기한다는 것을 알수 있을것입니다.

하지만 우리는 비동기로 실행하기 위해 async를 붙여 비동기 함수로 만들고, await로 대기했는데 왜 첫번째 코드는 3초가 걸리고, create_task로 task로 만들어야지만 2초가 걸리는 걸까요?

또, 태스크는 왜 task1 에서 대기하고 다시 task2에서 대기했는데 2초가 걸리는걸까요?

그 이유는 async def로 선언 된 함수를 호출하면, 코드가 실행 되지 않고 코루틴 객체를 리턴하기만 할 뿐이기 때문입니다. 그리고 create_task는 이 반환된 객체를 가지고 비동기 작업 객체인 태스크를 만들고, 실행합니다.

위의 코드에서 우리가 async def안에 작성한 코드가 실행되는 시점은 코루틴에서는

await say_after(1, "hello")

입니다. say_after(1, "hello") 호출로 코루틴 객체가 생성되고, await에 의해 함수 내 작성한 코드가 동기적으로 실행되면서 끝날 때 까지 대기합니다.

반면에 태스크는 

    task1 = asyncio.create_task(
        say_after(1, 'hello'))

이 부분에서 이미 코드를 비동기로 실행했습니다.

그리고 await task 는 그저 이미 실행한 코드를 대기 할 뿐입니다. 따라서 task1과 task2의 await 순서를 바꿔도 같은 결과를 얻습니다.

우리가 비동기로 여러 작업을 하기 위해서는 명시적으로 코루틴을 태스크로 만들어 줄 필요가 있습니다. 일반적으로 async 함수를 통해 생성된 코루틴은 await를 하면 실행되는 함수로 이해하셔도 될것입니다. 자세한 내용은 pep492를 참조하세요.

 

3. 비동기 프로그램의 흐름

await는 async 함수 내부에서만 사용 가능하며, 태스크는 비동기 함수 내에서만 생성할 수 있습니다. 따라서 태스크를 사용하여 비동기 작업을 하기 위해선, asyncio.Run(coroutine)을 사용하여 비동기 함수를 동기적으로 실행 시켜야합니다.

asyncio.run(main())

예제 코드에서 이 부분이 그러한데, main 함수 호출로 코루틴을 만들어서 실행시킵니다. main 내부에서 비동기 작업을 정의할 수 있습니다.

asyncio.run은 코루틴을 동기적으로, 그러니깐 코루틴이 끝날 때 까지 실행합니다. 비동기 함수 내부에서는 당연히 사용하지 않습니다. await가 있으니까요.

asyncio.run으로 실행된 비동기 함수는 create_task를 만나 해당 코루틴을 비동기적으로 실행합니다.

\

비동기 함수 내부에서 await를 만나면, 해당 코루틴, 태스크가 완료 될 때까지 대기하면서, 실행되지 않았던 코드들을 실행합니다. task1 에 대한 할당이 아직 실행되지 않았으니 main으로 복귀하여 계속됩니다.