8.2. 코루틴과 태스크

코루틴은 asyncio 프로그램이 구성되는 작업 단위이며, 태스크는 애플리케이션이 여러 코루틴을 동시에 실행하는 방법입니다.

8.2.1. 코루틴

코루틴(coroutine)async def 로 선언된 함수입니다:

import asyncio

async def heartbeat(interval_ms):
    while True:
        print("tick")
        await asyncio.sleep_ms(interval_ms)

본문은 await 라는 한 가지 추가 요소를 제외하면 평범한 함수처럼 보입니다. 코루틴이 무언가를 기다려야 하는 곳 — 슬립, 네트워크 읽기, 설정되는 이벤트 — 어디서든, 그것은 기다리는 대상이 준비될 때까지 코루틴을 일시 중단하는 방법을 아는 표현식을 await 합니다. 각 await 에서 코루틴은 제어권을 asyncio로 되돌려 주고, asyncio는 await된 연산이 완료되면 동일한 지점에서 코루틴을 재개합니다.

asyncio 모듈은 두 가지 슬립을 제공합니다:

  • asyncio.sleep() — 인자는 초 단위이며 float를 받습니다.

  • asyncio.sleep_ms() — 인자는 밀리초 단위이며 int를 받습니다. MicroPython 확장으로, 펌웨어의 타이밍 조절 값이 밀리초 형태이기 때문에 보통 카메라에서 적절한 선택입니다.

단순한 async def 는 그 자체로는 아무것도 하지 않습니다. heartbeat(500) 을 호출해도 본문이 실행되지 않으며, asyncio가 스케줄링해야 하는 코루틴 객체 를 반환합니다. 하나를 스케줄링하는 가장 간단한 방법은 asyncio.run() 입니다:

asyncio.run(heartbeat(500))

asyncio.run() 은 이벤트 루프를 시작하고, 넘겨받은 코루틴을 최상위 진입점으로 스케줄링하며, 그 코루틴이 반환될 때까지 루프를 구동한 다음, 루프를 해체합니다. 단일 코루틴의 경우 그것이 프로그램 전체입니다. 여러 코루틴의 경우 애플리케이션은 태스크를 사용합니다.

8.2.2. 태스크

태스크(task) 는 코루틴을 감싼 asyncio의 래퍼로, 이것을 현재 코루틴과 동시에 스케줄링하고 나는 계속 진행하게 해 달라 고 말하는 것입니다. asyncio.create_task() 는 하나를 만들고 스케줄링된 작업을 나타내는 Task 객체를 반환합니다

task = asyncio.create_task(heartbeat("fast", 100))

이제 코루틴은 루프의 스케줄에 올라가 있으며, 호출자는 그것을 기다리지 않았습니다. 반환된 Task 는 호출자가 이후에 그 실행 중인 작업과 상호작용하는 데 사용하는 핸들입니다.

애플리케이션이 핸들을 갖게 되면 그것으로 세 가지를 할 수 있습니다:

  • 태스크가 끝나기를 기다립니다. Task 자체가 awaitable입니다. result = await tasktask 의 코루틴이 반환될 때까지 현재 코루틴을 일시 중단한 다음, 그 코루틴이 반환한 값(또는 발생시킨 예외를 다시 발생)으로 재개합니다.

  • 태스크를 취소합니다. task.cancel() 은 태스크의 코루틴 내부에서 다음 await 시점에 asyncio.CancelledError 가 발생하도록 스케줄링하여, finally 블록에서 정리 코드를 실행할 기회를 줍니다. 자세한 내용은 타임아웃 및 취소 페이지에서 다룹니다.

  • 나중에 식별합니다. asyncio.current_task() 는 현재 실행 중인 코루틴의 Task 를 반환합니다. 대부분의 스크립트는 이것을 결코 호출하지 않으며, 계측 및 예외 핸들러에서 등장합니다.

스크립트가 매번 핸들을 캡처할 필요는 없습니다. 애플리케이션이 시작하고 그대로 실행하도록 두는 일회성 백그라운드 태스크는 반환 값을 버려도 됩니다 — 루프는 여전히 그것들을 스케줄링합니다:

import asyncio

async def heartbeat(name, interval_ms):
    while True:
        print(name)
        await asyncio.sleep_ms(interval_ms)

async def main():
    asyncio.create_task(heartbeat("fast", 100))
    asyncio.create_task(heartbeat("slow", 500))
    await asyncio.sleep(5)

asyncio.run(main())

두 개의 create_task 호출은 둘 중 어느 것도 기다리지 않고 두 하트비트를 모두 스케줄링합니다. 제어권은 즉시 main 으로 돌아오며, 그런 다음 5초 슬립을 awaits 합니다. 슬립하는 동안 두 하트비트 태스크가 진행되며, 루프는 실행 준비가 된 태스크를 차례로 순환합니다. 5초 후 main 이 반환되면, 루프는 여전히 살아 있는 태스크를 모두 해체하고, asyncio.run() 이 호출자에게 반환됩니다.

애플리케이션이 실제로 위의 세 가지 연산 중 하나가 필요할 때마다 핸들을 캡처하세요. 실제로는 그것이 거의 항상 을 의미하는데, 애플리케이션을 깔끔하게 종료한다는 것은 그것이 생성한 백그라운드 태스크를 취소하는 것을 뜻하기 때문입니다 — 취소 페이지에서 그 패턴을 다룹니다.

8.2.3. 두 줄 규칙

최소한의 asyncio 프로그램은 위의 예제들이 끝나는 그 두 줄 입니다:

async def main():
    ...

asyncio.run(main())

그 외 모든 것 – 애플리케이션이 생성하는 태스크, 그것들을 조율하는 데 쓰는 기본 요소, 여는 스트림 – 은 main 내부에서(그리고 main 이 생성하는 코루틴 내부에서) 일어납니다. 스크립트가 카메라의 고전적인 while True: csi0.snapshot() 루프를 넘어설 때, 답은 여러 곳에서 asyncio.run() 을 호출하는 것이 아니라, 새 작업을 더 많은 태스크로서 main 에 접어 넣는 것입니다.