ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [Python] Tip - 많은 함수를 동시에 실행하려면 코루틴을 고려
    언어/파이썬 & 장고 2017. 1. 6. 20:35

    파이썬 프로그래머는 스레드를 사용하여 여러 기능을 동시에 실행하는 것처럼 보여줄 수 있습니다. 하지만 스레드를 사용하는 데는 크게 세 가지 문제가 있습니다.

    1. 스레드들이 서로 안전하게 동작하도록 조율하려면 특별한 도구가 필요합니다. 따라서 스레드를 사용하는 코드가 절차적인 싱글 스레드 코드보다 이해하기 어렵습니다. 또한 이런 복잡성 때문에 시간이 지날수록 스레드 코드를 확장하거나 유지보수하기 어렵습니다.
    2. 스레드에는 메모리가 많이 필요합니다 (스레드당 약 8MB 정도). 많은 컴퓨터에서 스레드를 수십 개 가량 실행할 때는 문제가 되지 않습니다. 하지만 프로그램이 함수 수천 개를 동시에 실행한다면 문제가 발생합니다. 이러한 함수는 사용자가 서버에 보내는 요청, 화면의 픽셀, 시뮬레이션의 입자에 대응할지도 모릅니다. 고유의 활동마다 스레드를 하나씩 실행한다면 당연히 제대로 동작하지 않을 것입니다.
    3. 스레드를 시작하는 데는 비용이 많이 듭니다. 끊임없이 새 병행 함수를 생성하고 종료하면 스레드를 사용하면서 드는 부하가 커져서 전체 시스템이 느려집니다.


    파이썬에서는 코루틴(coroutine)으로 이런 문제를 모두 해결할 수 있습니다. 코루틴을 이용하면 파이썬 프로그램에서 동시에 많은 함수를 실행하는 것처럼 보이게 할 수 있습니다. 코루틴은 제너레이터를 확장하는 방법으로 구현합니다. 제너레이터 코루틴을 시작하는 데 드는 비용은 함수 호출입니다. 따라서 한 번 활성화되면 소진될 때까지 1KB 미만의 메모리만 소비합니다.

    코루틴이란?



    보통 함수는 입력으로 주어진 인수에 대해서 한 번만 실행됩니다. 하지만, 일련의 입력을 처리하도록 함수를 작성할 수도 있습니다. 이런 종류의 함수를 코루틴이라고 하고, 아래 예처럼 yield문을 표현식 형태로 사용해 생성할 수 있습니다.

    def print_matches(matchtext):
        print("Looking for", matchtext)
        while True:
            line = (yield)
            if matchtext in line:
                print(line)
    
    match = print_matches('good')
    next(match)
    # Looking for good
    match.send('Python is good')
    # Python is good
    match.close()

    코루틴은 send()로 값이 도착할 때까지 멈춰 있습니다. send()가 호출되면, 코루틴 안에서 (yield) 포현식에 의해 값이 반환되고 바로 다음 문장에 의해 처리됩니다. 이러한 처리는 다음 (yield) 표현식을 만날 때까지 계소되고, 그 순간 멈춘 함수가 다시 멈춥니다.


    코루틴은 제너레이터를 소비하는 코드에서 send 함수를 사용하여 역으로 제너레이터 함수의 각 yield 표현식에 값을 보낼 수 있게 하는 방법으로 동작합니다. 제너레이터 함수는 send 함수로 보낸 값을 대응하는 yield 표현식의 결과로 받습니다.

    def my_coroutine():
        while True:
            received = yield
            print('Received: ', received)
    it = my_coroutine()
    next(it) # 코루틴을 준비함
    it.send('First')
    it.send('Second')
    
    # 결과
    # ('Received: ', 'First')
    # ('Received: ', 'Second')


    제너레이터가 첫 번째 yield 표현식으로 전진해서 첫 번째 send의 값을 받을 수 있게 하려면 먼저 next를 호출해야 합니다. yield와 send의 조합은 제너레이터가 외부 입력에 반응하여 다음 번에 다른 값을 얻게 하는 표준 방법입니다. 예를 들어 지금까지 보낸 값 중에서 최솟값을 넘겨주는 제너레이터 코루틴을 구현한다고 가정합니다. 여기서 넘길 값이 없는 yield로 외부에서 보낸 초기 최솟값을 받아서 코루틴을 준비합니다. 이후 제너레이터는 반복적으로 다음 값을 받으면서 새 최솟값을 넘겨줍니다.

    def minimize():
        current = yield
        while True:
            value = yield current
            current = min(value, current)
     
    # 제너레이터를 소비하는 코드는 한 번에 한 단계씩 실행하여 각 입력을 받은 이후의 최솟값을 출력
    it = minimize()
    next(it) # 제너레이터를 준비함
    print(it.send(10))
    print(it.send(4))
    print(it.send(22))
    print(it.send(-1))
    
    
    # 결과
    # 10
    # 4
    # 4
    # -1


    제너레이터 함수는 send를 새로 호출할 때마다 전진하면서 계속 실행하는 것처럼 보입니다. 스레드와 마찬가지로 코루틴은 주변 환경에서 받은 입력을 소비하여 결과를 만들어낼 수 있는 독립적인 함수입니다. 둘의 차이는 코루틴이 제너레이터 함수의 각 yield 표현식에서 멈췄다가 외부에서 send를 호출할 때마다 다시 시작한다는 점입니다. 이게 바로 코루틴이 마법처럼 동작하는 메커니즘입니다. 

    이 동작 덕분에 제너레이터를 소비하는 코드에서 코루틴의 각 yield 표현식 이후에 원하는 처리를 할 수 있습니다. 제너레이터를 소비하는 코드는 제너레이터의 출력값으로 다른 함수를 호출하고 자료 구조를 수정할 수 있습니다. 가장 중요한 건 다른 제너레이터 함수들을 yield 표현식 이전까지 전진시킬 수 있다는 점입니다. 많은 별개의 제너레이터를 똑같은 방식으로 실행하면, 이 제너레이터들이 파이썬 스레드의 병행 동작을 흉내 내며 동시에 실행하는 것처럼 보입니다.

    요약

    코루틴은 함수 수만 개를 마치 동시에 실행하는 것처럼 실행하는 효과적인 방법을 제공

    제너레이터 안에서 yield 표현식의 값은 외부 코드에서 제너레이터의 send 메서드에 전달한 값

    코루틴은 프로그램의 핵심 로직을 주변 환경과 상호 작용하는 코드로부터 분리할 수 있는 강력한 도구


    댓글