어떠한 함수 안에 또 다른 함수가 있을 때, 첫번째 함수와 함수 안에 들어있는 함수를 각각
메인 루틴과 서브 루틴이라고 부른다. 예를 들어 다음과 같은 함수를 보자.
def add(a, b):
c = a + b # add 함수가 끝나면 변수와 계산식은 사라짐
print(c)
print('add 함수')
def calc():
add(1, 2) # add 함수가 끝나면 다시 calc 함수로 돌아옴
print('calc 함수')
calc()
calc함수에서 add함수를 호출했을 때, add함수가 끝나면 다시 calc함수로 돌아오게 된다. 이 관계를 메인 루틴, 서브 루틴이라고 한다면 calc 함수는 메인 루틴, add 함수는 서브 루틴이라고 할 수 있다. 메인루틴과 서브루틴의 호출 과정을 그림으로 보면 다음과 같다.
메인 루틴에서 서브 루틴을 호출하면 서브 루틴의 코드를 실행한 뒤 다시 메인 루틴으로 돌아온다. 이 과정에서 서브 루틴이 끝나면 서브 루틴의 내용은 모두 사라진다. 즉, 서브 루틴은 메인 루틴에 종속된 관계라고 할 수 있다.
코루틴coroutine
은 서브 루틴과 달리 메인 루틴과 협력한다는 루틴이라는 뜻이다. 즉, 서로 종속된 관계가 아니라 대등한 관계라고 할 수 있다. 코루틴은 여러번 호출될 수 있고 호출되면서 상태를 유지할 수 있다. 함수에서 코드를 실행하는 지점을 진입점(entry point)라고 하는데, 코루틴은 진입점이 여러 개인 함수이다. 코루틴의 호출 과정을 그림으로 보면 다음과 같다.
코루틴은 앞에서 살펴보았던 Generators
와 유사한데, 다음 코드를 살펴보자.
import time
def coroutine_test():
greeting = "good "
while True:
text = (yield greeting) # coroutine instance
print("text = ",end=""), print(text)
greeting += text
# main 함수에서 send() 메서드 실행 시 "morning", "afternoon", "evening"을 text에 저장
if __name__ == "__main__":
cr = coroutine_test()
print("cr=",end=""), print(cr)
next(cr)
time.sleep(2)
print("send 1")
print(cr.send("morning"))
time.sleep(2)
print("send 2")
print(cr.send("afternoon"))
time.sleep(2)
print("send 3")
print(cr.send("evening"))
time.sleep(2)
코루틴은 제너레이터와 비슷하게 yield
를 사용하나, 제너레이터는 yield
로 값을 산출하였지만 코루틴은 값을 받아온다는 차이점이 있다. 위 코드처럼 파이썬에서 코루틴은 yield
와 소괄호로 나타낼 수 있다.
주석에서 main 함수에서 send() 메서드 실행 시 "morning", "afternoon", "evening"을 text에 저장한다고 나왔는데, 코루틴은 여러번 호출 될 때 현재 상태를 유지하는 특징이 있어서 출력 때마다 "morning", "afternoon", "evening"이 붙여서 나오는 출력결과가 나왔다.
스레드, 프로세스에서 다뤄보았던 1억을 증가시키는 코드의 코루틴 버전으로 살펴보자.
def coroutine_1():
return_value = 0
while True:
input_value = (yield return_value)
return_value = input_value + 1
def coroutine_2():
return_value = 0
while True:
input_value = (yield return_value)
return_value = input_value + 1
if __name__ == "__main__":
ret_value = 0
c1 = coroutine_1()
c2 = coroutine_2()
next(c1)
next(c2)
while ret_value < 100000000:
ret_value = c1.send(ret_value)
ret_value = c2.send(ret_value)
print("ret_value =",end=""), print(ret_value)
print("end of main")
코루틴은 send()
메서드를 호출하기 전에는 코루틴이 수행되지 않고 while문의 yield에서 대기한다. 따라서 내가 어떤 로직을 수행하다가 원하는 시점에 send()
메서드를 호출해서 코루틴과 협력해서 작업을 할 수 있고, 또한 c1과 c2의 코루틴에 보내는 중간에 어떤 원하는 작업을 할 수 있다.
코루틴은 이처럼 앞서 살펴보았던 멀티태스킹 기법과 비교해서 자원의 동기화 문제에서 자유롭다는 장점이 있어서 협력형 멀티태스킹 활용에 유리하다.
asyncio
(Asynchronous I/O)는 코루틴과 관련된 async/await
구문을 사용하여 동시성 코드를 작성하는 라이브러리이며, 여러가지 파이썬의 비동기처리 프레임워크의 기반으로 사용된다. 이 라이브러리를 사용하면 CPU 작업과 I/O를 병렬로 처리하게 해준다. 앞서 살펴본 코루틴은 제너레이터 기반으로 작성되었는데, 제너레이터 기반 코루틴은 파이썬 3.10버전에서 삭제될 예정이므로 파이썬에서 코루틴을 활용하기 위해서는 asyncio
를 적극적으로 활용할 필요가 있다.
import time
def coroutine_test():
greeting = "good "
while True:
text = (yield greeting)
print("text = ",end=""), print(text)
greeting = "good "
# greeting의 초기화를 위해 무한히 반복하는 while문에 "good " string 추가
greeting += text
if __name__ == "__main__":
cr = coroutine_reset()
print("cr=",end=""), print(cr)
next(cr)
time.sleep(2)
print("send 1")
print(cr.send("morning"))
time.sleep(2)
print("send 2")
print(cr.send("afternoon"))
time.sleep(2)
print("send 3")
print(cr.send("evening"))
time.sleep(2)
import asyncio
import time
async def coroutine_1(): #async문으로 네이티브 코루틴 coroutine_1 정의
return_value = 0
while return_value < 50000000:
return_value += 1
return return_value
async def coroutine_2(): # coroutine_2 정의
return_value = 0
while return_value < 50000000:
return_value += 1
return return_value
async def main(): #최상위 진입점 main() 정의
co1= await coroutine_1() # await문으로 해당 객체가 끝날 때 까지 기다린 뒤 결과 반환
co2= await coroutine_2()
print("ret_value =",end=""), print(co1+co2) #코루틴의 결과값 더하기
print("end of main")
if __name__ == "__main__":
start_time = time.time()
asyncio.run(main()) #asyncio 프로그램 실행 후 결과 반환
print(f"--- {time.time() - start_time} seconds ---") # 처리 시간 계산