본문 바로가기

Minding's Programming/FastAPI

[FastAPI] 동기와 비동기, 코루틴 개념 정리

728x90
반응형

FastAPI를 본격적으로 사용해보기에 앞서 알아보아야 할 개념인 동기와 비동기의 차이, 그리고 코루틴 함수에 대해서 공부해보았다.

 

동기와 비동기

- 동기(sync)

'코드가 동기적으로 동작한다'라는 말의 뜻은 '코드가 반드시 작성된 순서로 실행된다.'라는 뜻과 동일하다고 생각하면 된다. A,B,C의 순서로 코드가 작성되어 있다면, A 함수가 실행되고 끝나면 B가 실행되는 플로우로 진행된다.

import time


def delivery(name, mealtime):
    print(f"{name}에게 배달완료!")
    time.sleep(mealtime)  # n초 대기
    print(f"{name} 식사완료, {mealtime}시간 소요...")
    print(f"{name} 그릇 수거 완료")


def main():
    delivery("A", 3)
    delivery("B", 3)
    delivery("C", 4)


if __name__ == "__main__":
    start = time.time()  # 시작 시간(해당 시점 현재시간)
    main()
    end = time.time()  # 끝난 시간(현재시간)
    print(end - start)

 

- 비동기(async)

위와 반대로 '코드가 반드시 작성된 순서로 실행되는 것이 아니다.'라는 뜻이다. 함수 A가 실행되는 동안 B가 실행되었다가 종료될 수도 있다. 함수가 동시에 시작되지만, 각 함수의 처리 상황에 따라 종료 시점이 달라질 수 있다.

import time
import asyncio


async def delivery(name, mealtime):
    print(f"{name}에게 배달완료!")
    await asyncio.sleep(mealtime)  # n초 대기
    print(f"{name} 식사완료, {mealtime}시간 소요...")
    print(f"{name} 그릇 수거 완료")


async def main():
    await asyncio.gather(
        delivery("A", 5),
        delivery("B", 3),
        delivery("C", 4),
    )


if __name__ == "__main__":
    start = time.time()  # 시작 시간(해당 시점 현재시간)
    asyncio.run(main())
    end = time.time()  # 끝난 시간(현재시간)
    print(end - start)

 

위 설명만 읽으면, 비동기 함수로 코드를 작성하는 것이 무조건 좋을 것이라고 생각할 수도 있다. 하지만 비동기 함수를 사용할 때에도 사용 목적과 코드 흐름의 상황을 잘 파악해야 한다. 예를 들어 수학 계산식을 작동 시키는 간단한 함수라면, 굳이 비동기함수를 작성할 필요가 없다. 오히려 유지보수만 어려워지는 결과를 내기도 한다. 각 상황과 목적에 따라 비동기함수 사용여부를 잘 파악해야 한다.

 

 

파이썬 코루틴

- 메인 루틴: 프로그램이 진행되는 플로우 그 자체라고 할 수 있다. 코드 상의 "if __name__ == __main__" 이 부분이라고 할 수 있다.

- 서브 루틴: 메인 루틴에 포함되거나 각 함수에 포함되는 함수 그 자체들이다.

 

그렇다면 코루틴은 무엇일까?

- 코루틴의 특징

  • 다양한 진입점과 다양한 탈출점이 있다. (일반 루틴의 경우 하나의 진입점(호출)과 하나의 탈출점(return)만 있음)
  • ex) 짜장면 배달/수거 시스템 - A에게 짜장면을 배달 후 식사하는 동안 B에게 배달 후 다시 A의 그릇을 수거하러 감 = A 함수 진입 -> 탈출 -> B 함수 진입 -> 탈출 -> A 함수 진입(그릇 수거하는 부분으로 중간 진입)

 

위와 같은 코루틴을 사용하기 위해서는 asyncio라는 라이브러리를 사용해주어야 하며, 비동기 코루틴 함수를 지정할 때에는 함수 선언 앞에 async라고 붙여주어야 한다.

 

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

위 코드는 await 이라는 표현식을 통해 코루틴 함수를 실행했다. 실행결과는 비동기 함수를 사용하지 않았을 때와 똑같을 것이다.

 

하지만 여러 개의 코루틴을 함수에 지정 예약하여(task1 등에 저장) 동시에 실행하는 asyncio.create_task() 함수를 사용한다면 차이가 생긴다.

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초 더 빠르게 실행이 완료된 모습을 볼 수 있다.

 

 

어웨이터블

하지만 모든 함수가 await 표현식으로 사용되는 것은 아니다. await 표현식에서 사용할 수 있는 '어웨이터블 객체'는 다음과 같다.

  • 코루틴 (async def 함수)
  • 태스크 (위에서 볼 수 있듯 asyncio.create_task()를 통해 코루틴을 예약해 놓은 함수)
  • 퓨처

해당 내용에 대한 자세한 설명은 Python 공식 문서를 통해 더 알아볼 수 있다. (아래 링크)

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

 

Coroutines and Tasks

This section outlines high-level asyncio APIs to work with coroutines and Tasks. Coroutines, Awaitables, Creating Tasks, Task Cancellation, Task Groups, Sleeping, Running Tasks Concurrently, Eager ...

docs.python.org

 

728x90