python3부터 비동기 프로그래밍을 지원한다. 이 글에서는 python에서 비동기 프로그래밍이 작동하는 방식과 사용법에 대해 알아보려 한다.

coroutine?

비동기 프로그래밍을 사용한적 없는 사람들을 위해 코루틴이라는 개념에 대해 설명할 필요가 있다.

파이썬 코딩 도장

coroutine은 cooperative routine을 의마하는데 서로 협력하는 루틴이라는 뜻이다. 즉, 메인 루틴과 서브 루틴처럼 종속된 관계가 아니라 서로 대등한 관계이며 특정 시점에 상대방의 코드를 실행할 수 있는 함수이다.

위 그림처럼 코루틴은 함수가 종료되지 않은 상태에서 메인 루틴의 코드를 실행한 뒤 다시 돌아와 코루틴 코드를 실행한다. 일반 함수의 경우 코드를 한번 실행시키면 끝이난 뒤에 다시 실행한다. 하지만 코루틴은 종료되지 않고 코루틴의 내용이 유지된채로 여러번 실행할 수 있다.(위 그림처럼 entry point가 여러개다)

코루틴에 값 보내기

코루틴과 generator의 사용법을 비교하자면, generator는 yield를 통해 발생시키지만 코루틴은 yield로 값을 받아올 수 있다. 값을 보내줄때는 send 메서드를 사용하며, send 메서드가 보낸 값을 받아오려면 (yield) 형식으로 yield를 괄호로 묶어준 뒤 변수에 저장하면 된다.

  • 코루틴객체.send(값)
  • 변수 = (yield)
def number_coroutine():
    while True:        # 코루틴을 계속 유지하기 위해 무한 루프 사용
        x = (yield)    # 코루틴 바깥에서 값을 받아옴, yield를 괄호로 묶어야 함
        print(x)
 
co = number_coroutine()
next(co)      # 코루틴 안의 yield까지 코드 실행(최초 실행)
 
co.send(1)    # 코루틴에 숫자 1을 보냄
co.send(2)    # 코루틴에 숫자 2을 보냄
co.send(3)    # 코루틴에 숫자 3을 보냄
1
2
3

위 코드의 실행 flow는 다음과 같다.

next(co)로 코루틴의 코드를 최초 실행하면 x = (yield)의 yeild에서 대기하고 다시 메인 루틴으로 돌안온다.

그다음 메인 루틴에서 co.send(1)로 1을 보내면 코루틴의 대기상태가 풀리고 x=(yield)의 x= 부분이 실행된 뒤 print로 숫자가 출력된다. 해당 코루틴은 while True:로 반복되는 구조이므로 다시 x = (yield)의 yeild에서 대기한다. 이 과정의 반복이다.

코루틴 바깥으로 값 전달하기

(yield 변수) 형식으로 yield에 변수를 지정한 뒤 괄호로 묶어주면 값을 받아오면서 바깥으로 값을 전달된다.

  • 변수 = (yield 변수)
  • 변수 = next(코루틴객체)
  • 변수 = 코루틴객체.send(값)
def sum_coroutine():
    total = 0
    while True:
        x = (yield total)    # 코루틴 바깥에서 값을 받아오면서 바깥으로 값을 전달
        total += x
 
co = sum_coroutine()
print(next(co))      # 0: 코루틴 안의 yield까지 코드를 실행하고 코루틴에서 나온 값 출력
 
print(co.send(1))    # 1: 코루틴에 숫자 1을 보내고 코루틴에서 나온 값 출력
print(co.send(2))    # 3: 코루틴에 숫자 2를 보내고 코루틴에서 나온 값 출력
print(co.send(3))    # 6: 코루틴에 숫자 3을 보내고 코루틴에서 나온 값 출력
0
1
3
6

위 코드의 실행 flow는 다음과 같다.

먼저 next(co)로 코루틴의 코드를 최초로 실행하면 x = (yield total)의 yield에서 total을 메인 루틴으로 전달하고 대기한다. 그다음에 메인 루틴에서 print(next(co))와 같이 코루틴에서 나온 값을 출력한다.

그리고 co.send(1)로 1을 보내면 코루틴은 대기 상태에서 풀리고 x = (yield total)의 x = 부분이 실행된 뒤 total += x로 숫자를 누적한다. while로 인해 이 과정의 반복이다.

코루틴을 종료

보통 코루틴은 실행 상태를 유지하기 위해 while True:를 사용해서 끝나지 않는 무한 루프로 동작한다. 만약 코루틴을 강제로 종료하고 싶다면 close() 메서드를 사용하면 된다.

  • 코루틴객체.close()
def number_coroutine():
    while True:
        x = (yield)
        print(x, end=' ')
 
co = number_coroutine()
next(co)
 
for i in range(20):
    co.send(i)
 
co.close()    # 코루틴 종료
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19

사실 파이썬 스크립트가 끝나면 코루틴도 끝나기 때문에 close를 사용하지 않은 것과 별 차이가 없다. 하지만 close는 코루틴의 종료 시점을 알아야 할 때 사용하면 편리하다.

하위 코루틴의 반환값 가져오기

generator에서 yield from을 사용하면 값을 바깥으로 여러 번 전달하지만, 코루틴에서는 yield from 에 코루틴를 지정하면 해당 코루틴에서 return으로 반환한 값을 가져온다.

  • 변수 = yield from 코루틴()
def accumulate():
    total = 0
    while True:
        x = (yield)         # 코루틴 바깥에서 값을 받아옴
        if x is None:       # 받아온 값이 None이면
            return total    # 합계 total을 반환
        total += x
 
def sum_coroutine():
    while True:
        total = yield from accumulate()    # accumulate의 반환값을 가져옴
        print(total)
 
co = sum_coroutine()
next(co)
 
for i in range(1, 11):    # 1부터 10까지 반복
    co.send(i)            # 코루틴 accumulate에 숫자를 보냄
co.send(None)             # 코루틴 accumulate에 None을 보내서 숫자 누적을 끝냄
 
for i in range(1, 101):   # 1부터 100까지 반복
    co.send(i)            # 코루틴 accumulate에 숫자를 보냄
co.send(None)             # 코루틴 accumulate에 None을 보내서 숫자 누적을 끝냄
55
5050

코루틴에 1부터 10까지 보내서 합계 55를 구하고, 다시 1부터 100까지 보내서 합계 5050을 구했다.

co = sum_coroutine() 에서 sum_coroutine 객체를 만들어 사용하지만 내부적으로는 total = yield from accumulate() 을 사용하여 코루틴 accumulate의 반환값을 가져와 사용한다.

하위 코루틴의 종료는 return을 사용하면 된다.(python 3.7부터 raise StopIteration을 사용하면 그냥 RuntimeError가 발생한다)

Reference