[WAFFLE] 04-2 Coroutine

3 minute read

Published:

In this post, concepts of Coroutine will be provided.

Subroutine(=Routine)

  • 프로그램의 호출 가능한 단위 (≈ 메서드, 함수, 프로시저)
  • 서브루틴을 호출하면, 해당 서브루틴이 끝나기까지 호출자(Caller)는 대기한다.
  • 서브루틴은 고유한 스택 프레임을 갖는다.

Coroutine

  • 코루틴은 중단/재개 가능한 서브루틴 (Non-preemptive Scheduling)
    • 여러개의 프로세스를 실행할 때 운영체제가 알아서 스케쥴링 해줌. 운영체제가 해주는 스케줄링은 어느 코드에서 멈추고 어느 코드에서 시작되는지 우리가 모름. 운영체제가 알아서 프로세스의 권한을 조작하는 스케줄링을 선점형 스케쥴링이라 함.
    • 반면 코루틴은 우리가 멈추고 우리가 재개함. 비선점형 스케쥴링이라고도 부름.
  • 경량 스레드라고도 불린다.
  • 동작이나 구현에 따라 여러 가지로 구분할 수 있다.
    • 중단 시 재개할 코루틴을 선택할 수 있으면 Symmetric Coroutine
    • 중단 시 Caller가 재개되면 Asymmetric Coroutine
    • 코루틴이 별도의 스택에서 실행되면 Stackful Coroutine
    • 코루틴이 Caller 스택 위에서 실행되면 Stackless Coroutine
  • 파이썬은 Asymmetric, Stackless Coroutine

Iterator

이터레이터도 코루틴이다. next()와 next() 사이에 이터레이터의 코드가 멈췄다가 실행되고 멈췄다고 실행됨.

class MyIterator:
    def __init__(self):
        self.values = [1,2]
        self.index = 0

    def __iter__(self):
        return self
    
    def __next__(self):
        if self.index >= len(self.values):
            raise StopIteration()
        value = self.values[self.index]
        self.index += 1
        return value

it = MyIterator()
a = next(it) # a = 1
b = next(it) # b = 2
c = next(it) # StopIteration 발생!

Generator

제너레이터도 코루틴이다.

def gen():
    # some task
    x = yield 1
    # some task
    yield x

it = gen()
a = next(it) # a = 1
it.send(10)
b = next(it) # b = 10
c = next(it) # StopIteration 발생!
  • asyncio 코루틴의 원시적인 형태
  • 비대칭 코루틴이며, 중단 및 재개 시마다 서로 데이터를 주고 받을 수 있다.

Asyncio

asyncio

  • Asynchronous I/O를 기반으로 작성된 코루틴 라이브러리
  • 내부적으로 Event Loop와 Generator를 기반으로 동작함
    • 왼쪽에 여러개의 task를 등록해두고 Event Loop가 각각의 task를 실행하다가 중단이 되면 다음 task를 실행시킴.
    • 중단이 발생하는 대부분의 경우는 I/O가 발생하는 것.
  • 순서와 관계 없이, 한 태스크가 I/O를 기다리는 동안 준비된 다른 태스크를 실행
import asyncio

async def main():
    print('Hello ...')
    await asyncio.sleep(1)
    print('... World!')

asyncio.run(main())
  • 함수 선언 시에 async 키워드를 붙인다.
  • 코루틴의 반환값을 얻으려면 await 키워드를 붙인다. return 값에도 await를 붙인다.
  • await 키워드가 사용되는 시점이 중단이 발생하는 시점
  • await 키워드는 코루틴 내부에서만 사용할 수 있다.
  • 일반 함수에서 코루틴 함수를 호출하려면, asyncio.run 등을 사용한다.
    • FastAPI 자체가 이미 asyncio 함수 루프 위에서 돌아가고 있기 때문에 쓸 일은 많이 없음.

FastAPI and Asyncio

  • FastAPI는 기본적으로 asyncio의 Event Loop위에서 실행된다.
  • 우리가 선언한 엔드포인트들이 async 함수냐 일반 함수냐에 따라 FastAPI 동작이 다른다.
    • async 함수인 경우 -> await 으로 호출
    • 일반 함수인 경우 -> threading 사용
  • 만약 async 키워드를 붙였는데 blocking io를 호출하면 서버 전체가 blocking이 됨. 그래서 우리가 어떤 엔드포인트 안에서 블로킹 io가 호출이 될 것 같다면 애초에 일반 함수로 선언하는 것이 좋음.
@app.get("/normal-endpoint")
def normal_endpoint():
    time.sleep(5)

@app.get("/async-endpoint")
async def async_endpoint():
    await asyncio.sleep(5)

@app.get("/dump-endpoint")
async def async_endpoint():
    time.sleep(5)
  • async 없이 쓰면 time.sleep 이라는 블로킹 io가 발생해도 다른 스레드에서 실행되고 있기 때문에 서버가 블로킹 되지 않음.
  • 만약 async 함수에서 blocking이 발생한다면? -> 해당 워커가 block됨
    • asyncio 내부에서 sleep이라는 명령어가 있음. 이는 비동기 sleep이기 때문에 문제가 없음.
    • 세 번째 처럼 적으면 안됨. 서버 전체가 블로킹 됨.
  • 따라서 blocking이 발생할 수 있는 경우, 별도 스레드에서 실행해야 함
    • 엔드 포인트 자체를 일반 함수로 선언하거나 run_in_executor 을 이용
    • asyncio도 호출하고 블로킹 io도 호출하는 경우 엔드포인드 자체는 async로 선언하되, 블로킹 io를 호출하는 부분만 run_in_executor 이용.

Leave a Comment