Skip to content

29. 코루틴(Coroutine)


1. 키워드

  • 코루틴(Coroutine)
  • 메인 루틴(Main Routine)과 서브 루틴(Sub Routine)


2. 코루틴 사용하기

  • 지금까지 함수를 호출한 뒤 함수가 끝나면 현재 코드로 다시 돌아왔다.


  • 예를 들어, 다음과 같이 calc 함수 안에서 add 함수를 호출했을 때 add 함수가 끝나면 다시 calc 함수로 돌아온다.
  • 그리고 add 함수가 끝나면 이 함수에 들어있던 변수와 계산식은 모두 사라진다.


def add(a, b):
    c = a + b
    print(c)
    print("add 함수")

def calc():
    add(1, 2)
    print("calc 함수")

calc()

# 3
# add 함수
# calc 함수


  • 이 소스 코드에서 calc 함수와 add 함수의 관계를 살펴보자.
  • calc가 메인 루틴이면 addcalc의 서브 루틴이다.


  • 이 메인 루틴과 서브 루틴의 동작 과정을 그림으로 나타내면 다음과 같은 모양이 된다.


001


  • 메인 루틴에서 서브 루틴을 호출하면 서브 루틴의 코드를 실행한 뒤 다시 메인 루틴으로 돌아온다.
  • 특히 서브 루틴이 끝나면 서브 루틴의 내용은 모두 사라진다.
  • 즉, 서브 루틴은 메인 루틴에 종속된 관계이다.
  • 하지만 코루틴은 방식이 조금 다르다.
  • 코루틴은 Cooperative Routine을 의미하는데, 서로 협력하는 루틴이라는 뜻이다.
  • 즉, 메인 루틴과 서브 루틴처럼 종속된 관계가 아니라 서로 대등한 관계이며 특정 시점에 상대방의 코드를 실행한다.


002


  • 이처럼 코루틴은 함수가 종료되지 않은 상태에서 메인 루틴의 코드를 실행한 뒤 다시 돌아와서 코루틴의 코드를 실행한다.
  • 따라서 코루틴이 종료되지 않았으므로 코루틴의 내용도 계속 유지된다.
  • 일반 함수를 호출하면 코드를 한 번만 실행할 수 있지만, 코루틴은 코드를 여러 번 실행할 수 있다.
  • 참고로 함수의 코드를 실행하는 지점을 진입점(Entry Point)이라고 하는데, 코루틴은 진입점이 여러 개인 함수이다.


3. 코루틴에 값 보내기

  • 코루틴은 제너레이터의 특별한 형태이다.
  • 제너레이터는 yield로 값을 발생시켰지만 코루틴은 yield로 값을 받아올 수 있다.


  • 다음과 같이 코루틴에 값을 보내면서 코드를 실행할 때는 send 메서드를 사용한다.
  • 그리고 send 메서드가 보낸 값을 받아오려면 (yield) 형식으로 yield를 괄호로 묶어준 뒤 변수에 저장한다.


코루틴객체.send()
변수 = (yield)


  • 그럼 코루틴에 숫자 1, 2, 3을 보내서 출력해 보자.


def number_coroutine():
    while True:
        x = (yield)
        print(x)

co = number_coroutine()
next(co)

co.send(1) # 1
co.send(2) # 2
co.send(3) # 3


  • 먼저 코루틴 number_coroutinewhile True:로 무한히 반복하도록 만든다.
  • 왜냐하면 코루틴을 종료하지 않고 계속 유지시키기 위해 무한 루프를 사용한다.
  • 그리고 x = (yield)와 같이 코루틴 바깥에서 보낸 값을 받아서 x에 저장하고, printx의 값을 출력한다.


def number_coroutine():
    while True:
        x = (yield)
        print(x)


  • 코루틴 바깥에서는 co = number_coroutine()과 같이 코루틴 객체를 생성한 뒤 next(co)(co.__next__()를 호출해도 상관없다.)로 코루틴 안의 코드를 최초로 실행하여 yield까지 코드를 실행한다.


next(코루틴객체)
코루틴객체.__next__()
co = number_coroutine()
next(co)


  • 그다음에 co.send로 숫자 1, 2, 3을 보내면 코루틴 안에서 숫자를 받은 뒤 print로 출력한다.


co.send(1) # 1
co.send(2) # 2
co.send(3) # 3


  • 이 코루틴의 동작 과정을 그림으로 살펴보자.
  • 먼저 next(co)로 코루틴의 코드를 최초로 실행하면 x = (yield)yield에서 대기하고 다시 메인 루틴으로 돌아온다.


003


  • 그다음에 메인 루틴에서 co.send(1)1을 보내면 코루틴은 대기 상태에서 풀리고 x = (yield)x = 부분이 실행된 뒤 print(x)로 숫자를 출력한다.
  • 이 코루틴은 while True:로 반복하는 구조이므로 다시 x = (yield)yield에서 대기한다.
  • 그러고 나서 메인 루틴으로 돌아온다.
  • 이런 과정으로 send가 보낸 값을 (yield)가 받게 된다.


004


  • 계속 같은 과정으로 send를 사용하여 값을 보내면 코루틴에서 값을 받아서 출력한다.
  • 정리하자면 next 함수(__next__ 메서드)로 코루틴의 코드를 최초로 실행하고, send 메서드로 코루틴에 값을 보내면서 대기하고 있던 코루틴의 코드를 다시 실행한다.


send로 코루틴의 코드를 최초로 실행하기

  • 지금까지 코루틴의 코드를 최초로 실행할 때 next 함수(__next__ 메서드)를 사용했지만, 코루틴객체.send(None)과 같이 send 메서드에 None을 지정해도 코루틴의 코드를 최초로 실행할 수 있다.


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

  • 지금까지 코루틴 안에 값을 보내기만 했는데 이번에는 코루틴에서 바깥으로 값을 전달해 보자.


  • 다음과 같이 (yield 변수) 형식으로 yield에 변수를 지정한 뒤 괄호로 묶어주면 값을 받아오면서 바깥으로 값을 전달한다.
  • 그리고 yield를 사용하여 바깥으로 전달한 값은 next 함수(__next__ 메서드)와 send 메서드의 반환값으로 나온다.


변수 = (yield 변수)
변수 = next(코루틴객체)
변수 = 코루틴객체.send()


  • 그럼 코루틴에 숫자를 보내고, 코루틴은 받은 숫자를 누적해서 바깥에 전달해 보자.


def sum_coroutine():
    total = 0
    while True:
        x = (yield total)
        total += x

co = sum_coroutine()
print(next(co)) # 0

print(co.send(1)) # 1
print(co.send(2)) # 3
print(co.send(3)) # 6


  • 코루틴에서 값을 누적할 변수 total을 만들고 0을 할당한다.
  • 그리고 x = (yield total)과 같이 값을 받아오면서 바깥으로 전달하도록 만든다.
  • 즉, 바깥에서 send가 보낸 값은 x에 저장되고, 코루틴 바깥으로 보낼 값은 total이다.
  • 그다음에 total += x와 같이 받은 값을 누적해 준다.


def sum_coroutine():
    total = 0
    while True:
        x = (yield total)
        total += x


  • 코루틴 바깥에서는 co = sum_coroutine()과 같이 코루틴 객체를 생성한 뒤 next(co)로 코루틴 안의 코드를 최초로 실행하여 yield까지 코드를 실행하고, printnext(co)에서 반화된 값을 출력한다.
  • 그다음에 co.send로 숫자 1, 2, 3을 보내고, printco.send에서 반환된 값을 출력한다.


co = sum_coroutine()
print(next(co)) # 0

print(co.send(1)) # 1
print(co.send(2)) # 3
print(co.send(3)) # 6


  • 참고로 nextsend의 차이를 살펴보면 next는 코루틴의 코드를 실행하지만 값을 보내지 않을 때 사용하고, send는 값을 보내면서 코루틴의 코드를 실행할 때 사용한다.


  • 이 코루틴의 동작 과정을 그림으로 살펴보자.
  • 먼저 next(co)로 코루틴의 코드를 최초로 실행하면 x = (yield total)yield에서 total을 메인 루틴으로 전달하고 대기한다.
  • 그다음에 메인 루틴에서 print(next(co))와 같이 코루틴에서 나온 값을 출력한다.
  • 여기서는 total0이 들어있으므로 0을 받아와서 출력한다.


005


  • 그리고 co.send(1)1을 보내면 코루틴은 대기 상태에서 풀리고 x = (yield total)x = 부분이 실행된 뒤 total += x로 숫자를 누적한다.
  • 이 코루틴은 while True:로 반복하는 구조이므로 다시 x = (yield total)yield에서 total을 메인 루틴으로 전달하고 대기한다.
  • 그다음에 메인 루틴에서 print(co.send(1))과 같이 코루틴에서 나온 값을 출력한다.
  • 여기서는 total1이 들어있으므로 1을 받아와서 출력한다.


006


  • 이런 과정으로 (yield total)이 바깥으로 전달한 값을 nextsend의 반환값으로 받고, send가 보낸 값을 x = (yield total)x가 받게 된다.
  • 여기서는 yield를 사용하여 코루틴 바깥으로 값을 전달하면 nextsend의 반환값으로 받는다는 점만 기억하면 된다.
  • 마지막으로 제너레이터와 코루틴의 차이점을 정리해 보자.


1] 제너레이터

  • next 함수(__next__ 메서드)를 반복 호출하여 값을 얻어내는 방식

2] 코루틴

  • next 함수(__next__ 메서드)를 한 번만 호출한 뒤 send로 값을 주고 받는 방식


값을 보내지 않고 코루틴의 코드 실행하기

  • 값을 보내지 않으면서 코루틴의 코드를 실행할 때는 next 함수(__next__ 메서드)만 사용하면 된다.
  • 잘 생각해 보면 이 방식이 일반적인 제너레이터이다.


5. 코루틴을 종료하고 예외 처리하기

  • 보통 코루틴은 실행 상태를 유지하기 위해 while True:를 사용하여 끝나지 않는 무한 루프로 동작한다.


  • 만약 코루틴을 강제로 종료하고 싶다면 다음과 같이 close 메서드를 사용한다.


코루틴객체.close()


  • 다음은 코루틴에 숫자를 20개 보낸 뒤 코루틴을 종료한다.


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 메서드를 사용하지 않은 것과 별 차이가 없지만, close 메서드는 코루틴의 종료 시점을 알아야 할 때 사용하면 편리하다.


1) GeneratorExit 예외 처리하기

  • 코루틴 객체에서 close 메서드를 호출하면 코루틴이 종료될 때 GeneratorExit 예외가 발생한다.
  • 따라서 이 예외를 처리하면 코루틴의 종료 시점을 알 수 있다.


def number_coroutine():
    try:
        while True:
            x = (yield)
            print(x, end=" ")
    except GeneratorExit:
        print()
        print("코루틴 종료")

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
# 코루틴 종료


  • 코루틴 안에서 try exceptGeneratorExit 예외가 발생하면 "코루틴 종료"가 출력되도록 만들었다.
  • 이렇게 하면 close 메서드로 코루틴을 종료할 때 원하는 코드를 실행할 수 있다.


    except GeneratorExit:
        print()
        print("코루틴 종료")


2) 코루틴 안에서 예외 발생시키기

  • 이번에는 코루틴 안에 특정 예외를 발생시켜서 코루틴을 종료해 보자.


  • 코루틴 안에 예외를 발생시킬 때는 다음와 같이 throw 메서드를 사용한다.
  • throw는 말 그대로 던지다라는 뜻인데 예외를 코루틴 안으로 던진다.
  • 이때 throw 메서드에 지정한 에러 메시지는 except as의 변수에 들어간다.


코루틴객체.throw(예외이름, 에러메시지)


  • 다음은 코루틴에 숫자를 보내서 누적하다가 RuntimeError 예외가 발생하면 에러 메시지를 출력하고 누적된 값을 코루틴 바깥으로 전달한다.


def sum_coroutine():
    try:
        total = 0
        while True:
            x = (yield)
            total += x
    except RuntimeError as e:
        print(e)
        yield total

co = sum_coroutine()
next(co)

for i in range(20):
    co.send(i)

print(co.throw(RuntimeError, "예외로 코루틴 끝내기"))

# 예외로 코루틴 끝내기
# 190


  • 코루틴 안에서 try exceptRuntimeError 예외가 발생하면 print로 에러 메시지를 출력하고 yield를 사용하여 total을 바깥으로 전달하도록 만들었다.


    except RuntimeError as e:
        print(e)
        yield total


  • 코루틴 바깥에서는 co.throw(RuntimeError, "예외로 코루틴 끝내기")와 같이 throw 메서드에 RuntimeError 예외와 에러 메시지를 지정하면 코루틴 안에서 예외가 발생한다.
  • 그리고 코루틴 안의 except에서 yield를 사용하여 바깥으로 전달한 값은 throw 메서드의 반환값으로 나온다.
  • 여기서는 코루틴 안에서 예외 처리를 했으므로 "예외로 코루틴 끝내기"가 출력되고, 코루틴 바깥에서는 누적된 값 190이 출력된다.


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

  • 제너레이터에서 yield from을 사용하면 값을 바깥으로 여러 번 전달할 수 있었다.
  • 하지만 코루틴에서는 조금 다르게 사용하는데, yield from에 코루틴을 지정하면 해당 코루틴에서 return으로 반환한 값을 가져온다.


변수 = yield from 코루틴()


  • 다음은 코루틴에서 숫자를 누적한 뒤 합계를 yield from으로 가져온다.


def accumulate():
    total = 0
    while True:
        x = (yield)
        if x is None:
            return total
        total += x

def sum_coroutine():
    while True:
        total = yield from accumulate()
        print(total)

co = sum_coroutine()
next(co)

for i in range(1, 11):
    co.send(i)
co.send(None) # 55

for i in range(1, 101):
    co.send(i)
co.send(None) # 5050


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


  • 먼저 다음과 같이 숫자를 받아서 누적할 코루틴을 만든다.
  • x = (yield)와 같이 코루틴 바깥에서 값을 받아온 뒤 total에 계속 더한다.
  • 특히 이 코루틴은 while True:로 무한히 반복하지만 코루틴을 끝낼 방법이 필요하다.
  • 여기서는 바깥에서 받아온 값이 None이면 return으로 total을 반환하고 코루틴을 끝낸다.


def accumulate():
    total = 0
    while True:
        x = (yield)
        if x is None:
            return total
        total += x


  • 이제 합계를 출력할 코루틴을 만든다.
  • 먼저 while True:로 무한히 반복한다.
  • 그리고 total = yield from accumulate()와 같이 yield from을 사용하여 코루틴 accumulate의 반환값을 가져온다.


def sum_coroutine():
    while True:
        total = yield from accumulate()
        print(total)


  • 코루틴에서 yield from을 사용하면 코루틴 바깥에서 send로 하위 코루틴까지 값을 보낼 수 있다.
  • 따라서 co = sum_coroutine()으로 코루틴 객체를 만든 뒤 co.send로 값을 보내면 accumulate에서 값을 받는다.


co = sum_coroutine()
next(co)

for i in range(1, 11):
    co.send(i) # 55


  • co.send로 숫자를 계속 보내다가 누적을 끝내고 싶으면 None을 보내면 된다.


co.send(None)


  • 이때 accumulateNone을 받으면 코루틴이 완전히 끝나지만 sum_coroutine에서 무한 루프로 반복하고 있으므로 printtotal을 출력한 뒤 다시 yield from accumulate()accumulate를 실행하게 된다.


def sum_coroutine():
    while True:
        total = yield from accumulate()
        print(total)


코루틴의 yield from으로 값을 발생시키기

  • 이번 예제에서 하위 코루틴은 x = (yield)와 같이 코루틴 바깥에서 보낸 값만 받아왔다.
  • 하지만 코루틴에서 yield에 값을 지정해서 바깥으로 전달했다면 yield from은 해당 값을 다시 바깥으로 전달한다.


def number_coroutine():
    x = None
    while True:
        x = (yield x)
        if x == 3:
            return x

def print_coroutine():
    while True:
        x = yield from number_coroutine()
        print(f"print_coroutine: {x}")

co = print_coroutine()
next(co)

x = co.send(1)
print(x) # 1
x = co.send(2)
print(x) # 2
co.send(3) # print_coroutine: 3

References