타임트리

[Python] 파이썬 코루틴 본문

Today I Learned/동시성 프로그래밍

[Python] 파이썬 코루틴

sean_j 2024. 5. 26. 20:49

먼저 코루틴을 보기 전에, 루틴을 정의하고 이로부터 파생되는 메인 루틴과 서브 루틴을 살펴보자.

  • 루틴: 일련의 명령 즉, 코드의 흐름
  • 메인 루틴: 프로그램의 메인 코드 흐름
  • 서브 루틴: 하나의 진입점(args)과 하나의 탈출점(return)이 있는 루틴으로, 흔히 아는 함수나 메서드 (메인 루틴의 보조)

그럼 메인 루틴과 서브 루틴의 관점에서 이전에 작성했던 코드를 다시 살펴보자. 아래 코드에서 메인 루틴은 if __name__=="__main__" 절의 내용이며, 여기서 main()delivery() 두 개의 서브 루틴이 존재한다.

import time

# 서브 루틴 1
def delivery(name, mealtime):
    print(f"{name}에게 배달 완료")
    time.sleep(mealtime)
    print(f"{name} 식사 완료, {mealtime}초 소요...")
    print(f"{name} 그릇 수거 완료")

# 서브 루틴 2
def main():
    delivery("A", 5)
    delivery("B", 3)
    delivery("C", 4)


# 메인 루틴
if __name__=="__main__":
    start = time.time()
    main()
    end = time.time()
    print("총 소요시간: {:.3f}초".format(end-start))

코루틴

그럼 코루틴은 무엇일까? 코루틴은 다음과 같이 설명할 수 있다.

  • 서브 루틴의 일반화된 형태
  • 다양한 진입점과 다양한 탈출점이 존재하는 루틴

아래의 비동기 함수 delivery()는 진입접과 탈출점이 2개씩 존재한다. 즉, 함수 호출 부분으로 진입과 return 으로 탈출하는 부분 그리고 추가적으로 await으로도 진입과 탈출을 할 수 있다.

import time
import asyncio


async def delivery(name, mealtime):    # 진입접 1
    print(f"{name}에게 배달 완료")
    await asyncio.sleep(mealtime)    # 탈출점 1, 진입점 2
    print(f"{name} 식사 완료, {mealtime}초 소요...")
    print(f"{name} 그릇 수거 완료")
    return None    # 탈출점 2


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("총 소요시간: {:.3f}초".format(end-start))
A에게 배달 완료
B에게 배달 완료
C에게 배달 완료
B 식사 완료, 3초 소요...
B 그릇 수거 완료
C 식사 완료, 4초 소요...
C 그릇 수거 완료
A 식사 완료, 5초 소요...
A 그릇 수거 완료
총 소요시간: 5.008초

 

이해를 위해 메인 루틴이 실행되는 과정을 출력과 함께 살펴보자.

delivery("A", 5) 진입 / await 탈출 → delivery("B", 3) 진입 / await 탈출 → delivery("C", 4) 진입 / await 탈출→ delivery("A", 5)await 진입 및 return 탈출 ...

 

즉, async, await 문법을 활용하여 진입점과 탈출점이 다양하도록 코루틴 함수를 작성하게 된다.

여러 개의 진입점과 탈출점을 가지는 함수를 코루틴 함수(coroutine function)라고 하며, 코루틴 함수로 비동기 함수를 작성한다!


그럼 파이썬 공식문서 코루틴과 태스크 부분을 참고해 코루틴을 확인해보자.

  • 아래와 같이 하나의 서브 루틴 hello_world()를 작성하고 이를 print 하는 메인 루틴을 작성해보자.
def hello_world():
    print("hello world!")
    return 123


if __name__ == "__main__":
    hello_world()
# 결과
hello world!
  • 위 코드를 코루틴으로 작성해보자. 먼저 hello_word() 함수 앞에 async를 붙여보자. 그러면 다음과 같이 코루틴 hello_worldawait 되지 않았다는 RuntimeWarning이 발생한다.
async def hello_world():
    print("hello world!")
    return 123


if __name__ == "__main__":
    hello_world()
# 결과
RuntimeWarning: coroutine 'hello_world' was never awaited hello_world()
RuntimeWarning: Enable tracemalloc to get the object allocation traceback
  • 그럼 메인 루틴에서 코루틴 함수 앞에 await를 붙이면 어떻게 될까? 이번에는 await가 코루틴 함수 바깥에서 실행되었다는 구문 오류가 발생한다.
async def hello_world():
    print("hello world!")
    return 123


if __name__ == "__main__":
    await hello_world()
# 결과
SyntaxError: 'await' outside function
  • 위 오류가 지적하는 것처럼, await 키워드는 async 키워드 안에서 사용되어야 한다. 즉, await 키워드는 코루틴 안에서 사용이 되어야 한다.
  • asyncio 패키지는 await을 코루틴 안에서 사용하도록 도와주는 패키지다.
  • asyncio를 사용해 위 함수를 아래와 같이 수정하고 실행해보자.
import asyncio


async def hello_world():
    print("hello world!")
    return 123


if __name__ == "__main__":
    asyncio.run(hello_world())
# 결과
hello world!
  • 이번에는 메인 루틴이 잘 수행되는 것을 확인할 수 있다. 그런데, 여기서 await의 위치는 코루틴 내부 어디에 붙여줘야 할까?
    • 만약 코루틴 함수 안에 await print("hello world!") 로 수정한다면 TypeError: object NoneType can't be used in 'await' expression 가 발생한다.
    • 즉, None type은 await 표현을 사용할 수 없다.

 

async, await 기본 사용법

그럼 await 뒤에는 어떤 객체가 와야할까?

await은 awaitable 객체에 사용할 수 있다.

  • 코루틴 함수는 awaitable이므로 다른 코루틴에서 기다릴 수 있다!
  • awaitable 객체: 코루틴(coroutine), 태스크(task), 퓨쳐(future)
await 코루틴의 경우

아래 코드에서 await이 붙은 부분은 총 4개가 있는데 하나씩 살펴보자.

  • await asyncio.sleep(): asyncio.sleep() 메소드는 프로그램을 블로킹 시키는 메소드로 코루틴 함수이자 awaitable 객체, 만약 time.sleep() 메서드로 수정하면 이는 코루틴 함수가 아니기 때문에 error 발생
  • await delivery(): delivery() 함수는 코루틴 함수이므로 awaitable 객체
async def delivery(name, mealtime):
    print(f"{name}에게 배달 완료")
    await asyncio.sleep(mealtime)  # asyncio.sleep() 메소드는 
    print(f"{name} 식사 완료, {mealtime}초 소요...")
    print(f"{name} 그릇 수거 완료")


async def main():
    await delivery("A", 5)    # delivery()는 코루틴 함수(awaitable 객체)이므로 await 가능
    await delivery("B", 3)
    await delivery("C", 4)
await task의 경우

task는 코루틴을 동시에 예약하는데 사용한다. 즉 await 코루틴 을 사용하기 전 미리 task를 선언하고 이를 필요할 때 await로 호출하는 방식을 위해 사용한다.

async def main():
    task1 = asyncio.create_task(delivery("A", 5))
    task2 = asyncio.create_task(delivery("B", 3))
    ...
    await task2
    await task1

# 아래와 동일
async def main():
    ...
    await delivery("A", 5) 
    await delivery("B", 3)

 

await future의 경우 추후 멀티스레딩과 멀티 프로세싱을 볼 때 함께 살펴보도록 한다.

asyncio.run(코루틴, debug=False)

  • asyncio.run()은 코루틴을 실행하고 결과를 반환하는 함수

asyncio.gather()

  • asyncio.gather()는 동시에 태스크를 실행한 후, 각 태스크의 결과값을 리스트로 반환하는 함수
  • 동시성 프로그래밍을 수행하도록 도와주는 함수
async def main():
    await asyncio.gather(   # 비동기함수 동시 실행
        delivery("A", 5),
        delivery("B", 3),
        delivery("C", 4)
    )

 

위 코드에서 awaitable 객체 인자들을 전달해주면, 들어온 awaitable 객체를 동시에 작동시킨다.

위 코드를 아래와 같이 수정해서 실행해보자.

import time
import asyncio


# 코루틴 - awaitable 객체
async def delivery(name, mealtime):
    print(f"{name}에게 배달 완료")
    await asyncio.sleep(mealtime)
    print(f"{name} 식사 완료, {mealtime}초 소요...")
    print(f"{name} 그릇 수거 완료")
    return mealtime 


async def main():
    result = await asyncio.gather(   # 비동기함수 동시 실행 후 결과값을 리스트로 반환
        delivery("A", 5),
        delivery("B", 3),
        delivery("C", 4)
    )

    print(result)    # result 출력


if __name__=="__main__":
    start = time.time()
    asyncio.run(main())
    end = time.time()
    print("총 소요시간: {:.3f}초".format(end-start))
# 결과
A에게 배달 완료
B에게 배달 완료
C에게 배달 완료
B 식사 완료, 3초 소요...
B 그릇 수거 완료
C 식사 완료, 4초 소요...
C 그릇 수거 완료
A 식사 완료, 5초 소요...
A 그릇 수거 완료
[5, 3, 4]           <---- result 출력 결과
총 소요시간: 5.014초

 

 

---

참고: 인프런(파이썬 동시성 프로그래밍 : 데이터 수집부터 웹 개발까지 (feat. FastAPI))