상세 컨텐츠

본문 제목

코루틴 (Coroutine) (3) - 개념, 장점, 구현

개발/python-병렬처리

by Matthew0633 2022. 5. 11. 15:56

본문

코루틴이란?

스레드는 OS에서 관리하는 프로세스 내 작업 단위이다. 일반적으로 멀티스레드를 사용하여, CPU 코어에서 실시간, 시분할 비동기 작업을 수행하며 작업의 효율성을 높인다

그런데 단일(싱글)스레드 환경에서도 비동기 작업을 가능하게 하는 설계패턴이 코루틴이다. 단일 작업 (work1, work2, ...) 수행이 정의된 여러 개의 코루틴 함수를 설계하고 이들을 각각 서브루틴으로서 제어하면서 동시에 싱글스레드가 각 서브루틴을 오가며, 동시 작업이 가능한 형태이다.

코루틴을 제어할 때 서브루틴에서는 yield 키워드를, 메인루틴에서는 send 메소드를 사용한다. yield 키워드는 실행지점의 상태를 저장하기 때문에, 메인루틴과 서브루틴 간 양방향 전송이 가능하게 한다

코루틴을 통한 동시 작업 과정

메인루틴이 함수 호출 → 서브루틴에서 함수를 수행 → yield를 만나면 값을 메인루틴에 반환 후, 대기상태로 전환 → send()를 통해 메인루틴에서 값을 받을 때 다시 다음 yield 지점까지 서브루틴 실행 (없을 시 StopIteration 발생시키며 종료)

코루틴의 장점

코루틴의 장점은 싱글스레드에서의 동시 작업 수행을 통해 멀티스레드 환경의 단점들을 극복한 부분이다.

싱글스레드에서는 멀티스레드 사용보다 context switching 비용과 오버헤드가 감소하므로, 적은 자원이 사용될 가능성이 높아진다

또한 멀티스레드에서는 스레드 간 메모리를 공유하는 특성 때문에, synchronization 을 정교하게 처리하지 않는다면, 여러 스레드가 공유자원에 접근할 때 교착상태 (deadlock) 가 발생할 수 있다. 싱글스레드 환경에서는 교착상태의 발생 가능성 또한 줄일 수 있다.

이러한 점들이 코루틴을 통한 싱글스레드 환경에서의 동시성 사용에 대한 장점이라고 할 수 있다.

코루틴 예시 : yield, send, generator 함수를 활용한 기본 예시

그럼 코루틴을 한번 살펴보자. yield keyword를 사용하는 순간, generator 함수가 되기 때문에 해외에서는 코루틴을 generator 기반 코루틴으로 표현하는 경우도 있다고 한다

def coroutine1():
		# 서브루틴
    print('>>> coroutine started.')
    i = yield
    print('>>> coroutine received : {}'.format(i))

# 메인루틴
cr1 = coroutine1()
print(cr1, type(cr1))

next(cr1) # yield 지점까지 서브루틴 실행
next(cr1) # 메인루틴으로부터 send 받은 값이 없으므로, i에 None이 할당됨, StopIteration 발생 (yield 없으므로)

# print('>>> coroutine received : None')

cr2 = coroutine1()
next(cr2) # 반드시 next 함수로 yield 지점까지 도달해줘야 한다
cr2.send(100)
# print('>>> coroutine received : 100')

yield와 send 동작과, 메인루틴과 서브루틴 간 상호작용을 아래 코드를 통해 자세히 살펴보자.

코루틴을 통해 정의된 서브루틴에서는 첫 next 함수 실행 ( next(cr3) ) 을 해야 첫번째 yield 지점 ( yield x )까지 도달하여 yield 대기를 할 수 있다 (GEN_SUSPENDED). yield 값 ( x )을 메인루틴에 전달하고 해당 지점 (코드 라인이라고 이해하면 편하다) 에서 다시 대기한다. (GEN_SUSPENDED)

그리고 메인루틴이 다른 서브루틴을 수행하다가 해당 서브루틴 작업 재개를 위해, send로 서브루틴에 값을 보내게 되면 ( cr3.send(15) ), 대기하던 yield 지점의 변수 할당 부분 ( y = )에 send 값을 받고, 다음 yield 지점 ( yield x + y )까지 코드를 실행한 후 yield 값 ( x + y )을 메인루틴에 반환한다. 그리고 다시 해당 서브루틴은 대기 상태로 전환된다 (GEN_SUSPENDED)

# GEN_CREATED : 처음 대기 상태
# GEN_RUNNING : 실행 상태
# GEN_SUSPENDED : yield 대기상태
# GEN_CLOSED : 실행 완료 상태

def coroutine2(x):
    print('>>> coroutine started : {}'.format(x))
    y = yield x
    print('>>> coroutine received : {}'.format(y))
    z = yield x + y
    print('>>> coroutine received : {}'.format(z))

cr3 = coroutine2(10)

from inspect import getgeneratorstate

print(getgeneratorstate(cr3)) # GEN_CREATED
"""
GEN_CREATED
"""

print(next(cr3)) # 10
"""
>>> coroutine started : 10
10
"""

print(getgeneratorstate(cr3)) 
"""
GEN_SUSPENDED
"""

print(cr3.send(15)) # 15
"""
>>> coroutine received : 15
25
"""
print(cr3.send(20)) # 15
"""
>>> coroutine received : 20
Exception : StopIteration
"""

정리하자면, 특정 서브루틴을 코루틴객체로 처음 정의했을 때는 next 함수를 실행해야 서브루틴 수행이 시작된다. 서브루틴에서는 yield 를 통해 값을 메인루틴에 전달하고, 메인루틴에서는 send를 통해 서브루틴에 값을 전달하는 동시에 서브루틴 내에 다음 yield 값을 반환받는 형태로 상호작용이 이루어진다.

def → async, yield → await 으로 사용했을 때 StopIteration 예외처리가 가능한데 이는 나중에 다시 살펴볼 계획이다.

for loop + yield vs yield from

generator 안에서 반복문을 통한 여러번의 yield를 정의할 때, yield from iterable 객체 로 간단히 정의할 수 있다.

def generator1():
    for x in 'AB':
        yield x
    for y in range(1,4):
        yield y

def generator2():
    yield from 'AB'
    yield from range(1,4)

t3 = generator2()

print(next(t3))
print(next(t3))
print(next(t3))
print(next(t3))
print(next(t3)) # StopIteration

관련글 더보기

댓글 영역