Notice
Recent Posts
Recent Comments
Link
«   2024/05   »
1 2 3 4
5 6 7 8 9 10 11
12 13 14 15 16 17 18
19 20 21 22 23 24 25
26 27 28 29 30 31
Archives
Today
Total
05-16 01:30
관리 메뉴

nomad-programmer

[Programming/Python] 코루틴 (coroutine) 본문

Programming/Python

[Programming/Python] 코루틴 (coroutine)

scii 2023. 1. 29. 23:04

코루틴의 이점은 무엇인가?

보통 두 루틴이 동시에 실행되는 멀티태스킹은 하나의 변수에 값을 동시에 쓰면(write) 동기화 문제가 발생한다. 따라서 크리티컬 섹션, 세마포어, 뮤텍스 등 동기화를 위한 락(lock)이 필요하다. 하지만 코루틴은 시분할 방식 멀티태스킹이라 동기화를 위한 락이 필요하지 않다.


함수를 호출한 뒤 함수가 끝나면 현재 코드로 다시 돌아왔다. 예를 들어 다음의 코드를 보자.

def add(a, b):
    c = a + b
    print(c)
    print('add func')
    
def calc():
    add(1, 2)
    print('calc func')

이 소스 코드에서 calc함수와 add 함수의 관계를 살펴보자. calc가 메인 루틴(main routine)이면 add는 calc의 서브 루틴(sub routine)이다. 이 메인 루틴과 서브 루틴의 동작 과정을 그림으로 나타내면 다음과 같은 모양이 된다.

메인 루틴과 서브 루틴의 동작 과정

메인 루틴에서 서브 루틴을 호출하면 서브 루틴의 코드를 실행한 뒤 다시 메인 루틴으로 돌아온다. 특히 서브 루틴이 끝나면 서브 루틴의 내용은 모두 사라진다. 즉, 서브 루틴은 메인 루틴에 종속된 관계이다.

하지만 코루틴은 방식이 조금 다르다. 코루틴(coroutine)은 cooperative routine을 의미하는데 서로 협력하는 루틴이라는 뜻이다. 즉, 메인 루틴과 서브 루틴처럼 종속된 관계가 아니라 서로 대등한 관계이며 특정 시점에 상대방의 코드를 실행한다.

코루틴의 동작 과정

코루틴은 함수가 종료되지 않은 상태에서 메인 루틴의 코드를 실행한 뒤 다시 돌아와서 코루틴의 코드를 실행한다. 따라서 코루틴이 종료되지 않았으므로 코루틴의 내용도 계속 유지된다.

일반 함수를 호출하면 코드를 한 번만 실행할 수 있지만, 코루틴은 코드를 여러 번 실행할 수 있다. 참고로 함수의 코드를 실행하는 지점을 진입점(entry ponit)이라고 하는데, 코루틴은 진입점이 여러 개인 함수이다.

코루틴에 값 보내기

코루틴은 제네레이터의 특별한 형태이다. 제네레이터는 yield로 값을 발생시켰지만 코루틴은 yield로 값을 받아 올 수 있다. 다음과 같이 코루틴에 값을 보내면서 코드를 실행할 때는 send 메소드를 사용한다. 그리고 send 메서드가 보낸 값을 받아오려면 (yield) 형식으로 yield를 괄호로 묶어준 뒤 변수에 저장한다.

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

다음은 코루틴에 숫자 1, 2, 3을보내서 출력하는 예제이다.

def number_coroutine():
    # 코루틴을 계속 유지하기 위해 무한 루프 사용
    while True:
        # 코루틴 바깥에서 값을 받아옴, yield를 괄호로 묶어서 사용
        x = (yield)
        print(x)

co = number_coroutine()
next(co)    # 코루틴 안의 yield까지 코드 실행(최초 실행)

co.send(1)  # 코루틴에 숫자 1을 보냄
co.send(2)  # 코루틴에 숫자 2을 보냄
co.send(3)  # 코루틴에 숫자 3을 보냄


# 결과
1
2
3

먼저 while True로 무한 루프를 만든다. 왜냐하면 코루틴을 종료하지 않고 계속 유지시켜야 하기 때문이다. 그리고 x = (yield)와 같이 코루틴 바깥에서 보낸 값을 받아서 x에 저장하고 print로 x의 값을 출력한다.

코루틴 바깥에서는 co = number_coroutine() 과 같이 코루틴 객체를 생성한 뒤 next(co)로 코루틴 안의 코드를 최초로 실행하여 yield까지 코드를 실행한다.
그 다음에 co.send로 숫자를 보내면 코루틴 안에서 숫자를 받은 뒤 print로 출력한다.

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

코루틴의 과정

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

정리하자면 next() 함수로 코루틴의 코드를 최초 실행하고 send 메서드로 코루틴에 값을 보내면서 대기하고 있던 코루틴의 코드를 다시 실행한다. 즉, 코루틴은 yield에서 함수 중간에 대기한 다음 메인 루틴을 실행하다가 다시 코루틴을 실행한다는 점이다.

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


코루틴 바깥으로 값 전달

지금까지 코루틴 안에 값을 보내기만 했는데 이번에는 코루틴에서 바깥으로 값을 전달해보자. 다음과 같이 (yield 변수) 형식으로 yield에 변수를 지정한 뒤 괄호로 묶어주면 값을 받아오면서 바깥으로 값을 전달한다. 그리고 yield를 사용하여 바깥으로 전달한 값은 next함수와 send 메서드의 반환값으로 나온다.

  • 변수 = (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

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

이 코루틴 동작 과정을 그림으로 살펴보자.

이런 과정으로 (yield total)이 바깥으로 전달한 값을 next와 send의 반환값으로 받고, send가 보낸 값을 x = (yield total)의 x가 받게 된다. 여기서는 yield를 사용하여 코루틴 바깥으로 값을 전달하면 next와 send의 반환값으로 받는 다는점만 기억하면 된다.

마지막으로 제네레이터와 코루틴의 차이점을 정리해보자.

  • 제네레이터는 next 함수(__next__ 메서드)를 반복 호출하여 값을 얻어내는 방식
  • 코루틴은 next 함수(__next__ 메서드)를 한 번만 호출한 뒤 send로 값을 주고받는 방식

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

보통 코루틴은 실행 상태를 유지하기 위해 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는 코루틴의 종료 시점을 알아야 할 때 사용하면 편리하다.


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)
    
    
# 결과
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 
코루틴 종료

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


코루틴 안에서 예외 발생시키는 방법

코루틴 안에 예외를 발생시킬 때는 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 except로 RuntimeError 예외가 발생하면 print로 에러 메시지를 출력하고 yield를 사용하여 total을 바깥으로 전달하도록 만들었다.

throw 메서드에 RuntimeError 예외와 에러 메시지를 지정하면 코루틴 안에서 예외가 발생한다. 그리고 코루틴 안의 except에서 yield를 사용하여 바깥으로 전달한 값은 throw 메서드의 반환값으로 나온다.

여기서는 코루틴 안에서 예외 처리를 했으므로 '예외로 코루틴 끝내기'가 출력되고, 코루틴 바깥에서 누적된 값 190이 출력된다.


하위 코루틴의 반환값 가져오는 방법

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

  • 변수 = yield from 코루틴()

다음은 코루틴에서 숫자를 누적한 뒤 합계를 yield from으로 가져오는 예제이다.

def accumulate():
    total = 0
    while True:
        x = (yield)         # 코루틴 바깥에서 값을 받아옴
        if x is None:
            return total    # 받아온 값이 None이면 total 반환
        total += x

def sum_coroutine():
    while True:
        # accumulate의 반환 값을 가져옴
        total = yield from accumulate()
        print(total)

co = sum_coroutine()
next(co)

for i in range(1, 11):
    co.send(i)      # 코루틴 accumulate에 숫자를 보냄
co.send(None)       # 코루틴 accumulate에 None을 보내서 숫자 누적 끝냄

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


# 결과
55
5050

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

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

코루틴에서 yield from을 사용하면 코루틴 바깥에서 send로 하위 코루틴까지 값을 보낼 수 있다. 따라서 co = sum_coroutine()으로 코루틴 객체를 만든 뒤 co.send로 값을 보내면 accumulate에서 값을 받는다.
co.send로 숫자를 계속 보내다가 누적을 끝내고 싶으면 None을 보내면 된다.

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


StopIteration 예외 발생시키는 방법

코루틴도 제네레이터이므로 return을 사용하면 StopIteration이 발생한다. 사실 코루틴에서 return 값은 raise StopIteration(값)과 동작이 같다. 따라서 raise로 예외를 직접 발생시키고 값을 지정하면 yield from으로 값을 가져올 수 있다.

  • raise StopIteration(값)
def accumulate():
    total = 0
    while True:
        x = (yield)         # 코루틴 바깥에서 값을 받아옴
        if x is None:
            raise StopIteration(total)  # StopIteration에 반환할 값을 지정
        total += x

def sum_coroutine():
    while True:
        # accumulate의 반환 값을 가져옴
        total = yield from accumulate()
        print(total)

co = sum_coroutine()
next(co)

for i in range(1, 11):
    co.send(i)      # 코루틴 accumulate에 숫자를 보냄
co.send(None)       # 코루틴 accumulate에 None을 보내서 숫자 누적 끝냄

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


# 결과
55
5050

accumulate에서 return total 대신 "raise StopIteration(total)" 을 사용했다. 이때도 yield from은 accumulate의 total을 가져온다.

지금까지 코루틴에 대해 설명하였다. 코루틴은 함수가 종료되지 않은 상태에서 값을 주고받을 수 있는 함수이며 이 과정에서 현재 코드의 실행을 대기하고 상대방의 코드를 실행한다는 점이 중요하다. 

보통 코루틴은 시간이 오래 걸리는 작업을 분할하여 처리할 때 사용하는데 주로 파일 처리, 네트워크 처리 등에 활용한다.


코루틴의 yield from으로 값 발생시키는 방법

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

def number_coroutine():
    x = None
    while True:
        # 코루틴 바깥에서 값을 받아오면서 바깥으로 값을 전달
        x = (yield x)
        if 3 == x:
            return x

def print_coroutine():
    while True:
        # 하위 코루틴의 yield에 지정된 값을 다시 바깥으로 전달
        x = yield from number_coroutine()
        print('print_coroutine: ', x)

co = print_coroutine()
next(co)

x = co.send(1)  # number_coroutine으로 1을 보냄
print(x)        # 1: number_coroutine의 yield에서 바깥으로 전달한 값
x = co.send(2)  # number_coroutine으로 2를 보냄
print(x)        # 2: number_coroutine의 yield에서 바깥으로 전달한 값
co.send(3)      # 3을 보내서 반환값을 출력하도록 만듦


# 결과
1
2
print_coroutine:  3

코루틴 초기화 자동화 (데코레이터 사용)

코루틴 객체를 생성한 뒤 next를 호출하는 모양이 마음에 들지 않는다면 다음과 같이 코루틴 초기화 데코레이터를 만들어서 사용하면 된다. 즉, 데코레이터 안에서 코루틴 객체를 만들고 next를 호출한 뒤 객체를 반환한다.

# 코루틴 초기화 데코레이터
def coroutine(func):
    def init(*args, **kwargs):
        co = func(*args, **kwargs)  # 코루틴 객체 생성
        next(co)                    # next 호출 (코루틴 실행)
        return co                   # 코루틴 객체 반환
    return init


@coroutine      # 코루틴 초기화 데코레이터 지정
def sum_coroutine():
    total = 0
    while True:
        x = (yield total)
        total += x

# 코루틴 객체 생성한 뒤 바로 사용
co = sum_coroutine()

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


# 결과
1
3
6
Comments