nathan_H

[Python] 비동기 프로그래밍 본문

Programming Laguage/Java

[Python] 비동기 프로그래밍

nathan_H 2019. 3. 6. 22:26

출처 - https://mingrammer.com/translation-asynchronous-python/


비동기 파이썬


intro 


프로그램이란 각 라인 별로 순서대로 실행이 되는 특성이 있다.

리소스를 가져오기 위해 원격 서버로 접속하는 코드 라인을 

가지고 있다는건 서버 연결을 대기하는동안 프로그램이 아무것도 하지 못함을 의미한다.

진행하기 위해 응답을 기다려야한다.

이런 상황을 표준 해결책은 threading이다.

스레드는 여러개를 동시에 사용하면 프로그램은 동시에 여러가지 일을 할 수 있다.

여러 스레드를 돌릴 수 있으며 각 스레드는 동시에 실행이 된다,

경쟁 조건, 데드락, 라이브락 기아 상태 등의 까다로운 문제들을 포함한 에러가 발생하기 쉽다.


[컨텍스 스위칭]


비동기 프로그래밍은 이러한 모든 문제를 방지 할 순 있지만

이는 사실 cpu 컨택스트 스위칭이라는 전혀 다른 문제를 위해 설계되었다.


다중 스레드가 실행중일 때, 각 cpu 코어는 한 번에 하나의 스레드만 돌릴 수 있다.

모든 스레드와 프로세스가 자원을 공유하기 위해 cpu 컨텍스트 전환이 자주 일어난다.

일을 매우 단순화 하기 위해, 

cpu는 임의의 간격으로 스레드의 모든 컨택스트 정보를 저장하고 

다른 스레드로 전환한다.

cpu는 정해지지 않은 간격마다 지속적으로 스레드간 전환을 한다.

스레드는 자원이며 공짜가 아니다.


비동기 프로그래밍은 기본적으로 cpu 보다 스레드와 

컨텍스트 스위칭을 관리하는 애플리케이션이 존재하는 소프트웨어/ 유저페이스의 스레딩이이다.

기본적으로 비동기 세상에서 컨택스트는 임의의 간격이 아닌 오직 정의된 전환점에서만 전환이 된다.



믿을 수 없을만큼 효율적인 비서


엄청나 멀티 태스킹이 가능한 비서.

시간을 낭비하지 않는 - 항상 모든 일을 잘 마치며, 

매사에 최선을 다하는 비서가 있다고 상상해보자. 

이 비서는 - ‘Bob’이라고 부르자 - 이를 달성하기위해 미친듯이 멀티태스킹을 할 것이다. 

Bob은 한 번에 해야하는 5개의 일을 가지고 있다 

- 전화 받기, 손님들 접수 받기, 항공편 예약하기, 미팅 스케쥴 조정하기 그리고 서류 제출하기. 

이제 이 상황이 낮은 트래픽의 환경에 있다고 상상해보자. 

따라서 전화 받기, 방문객, 그리고 미팅 요청들이 적으며 각 업무들은 서로 떨어져있다. 

Bob의 대부분의 시간은 서류를 제출하는동안 항공사와 전화를 하는데 소비될 것이다. 

이 모든건 꽤 일반적이며 상상하기 쉽다. 

전화가 오면, Bob은 잠시 항공사와의 전화를 보류하고, 전화에 응답을 한 후, 

다시 항공사와의 전화로 돌아갈 것이다. 

언제 어떤 업무가 Bob에게 도착하면, 서류 제출은 뒤로 미뤄질 수 있다. 

왜냐하면 이는 지금 당장 해결해야할 필요가 없기 때문이다. 

이것이 사람이 동시에 여러가지 일을 하는 방식이며, 적절한 때에 컨텍스트 스위칭이 일어난다.



스레딩 버전은 5명의 Bob이 각각 하나의 업무만 가진 것처럼 보일 것이다. 

하지만 어느 주어진 시간에는 단 한 가지 일을 하는것만 허용된다. 

여기엔 Bob이 일을 할 수 있게 제어하는 장치가 있을텐데, 

이는 작업들에 대해 아무것도 이해하지 못한다. 

이 장치는 업무들이 발생하게된 사건(이벤트)을 이해하지 못하기 때문에, 

만일 3개의 작업이 아무것도 하고있지 않더라도 5개의 작업들 사이를 지속적으로 전환할 것이다. 

예를 들어, 서류 제출 담당의 Bob에 인터럽트가 생기면 전화 받기 담당의 Bob이 무언가를 할 수 있게된다. 

하지만 전화 받기 담당의 Bob은 아무 할 일도 없으며, 

따라서 그는 잠자기 상태로 돌아간다. 

아무것도 하지 않는 3개의 업무들마저 찾아내기 위해 

모든 업무들간의 전환을 함으로써 시간 낭비가 발생한다. 

컨텍스트 스위칭의 약 57% (3/5보다 약간 낮음)가 무의미한 작업이 될 것이다. 

물론, CPU 컨텍스트 스위칭이 매우 빠르기는하지만 결코 공짜는 아니다.



이벤트 루프? 코루틴?


비동기 프로그래밍을 하기위한 한 가지 방법은 이벤트 루프를 사용하는 것이다. 

이벤트 루프란, 이벤트/잡(events/jobs)을 관리하는 큐가 있을 때, 

단지 큐에서 지속적으로 잡을 빼내고 이들을 실행해주는 루프와 정확히 같은 말이다. 

이 잡들을 코루틴이라고 부른다. 

이들은 큐에 넣을 수 있는 그 어떤 이벤트들을 포함하는 명령어들의 작은 집합이다.



기존 콜백의 문제점을 개선


더 나은 방법을 위해선 언어 자체의 지원이 필요하다. 

더 나은 비동기 프로그래밍을 위해선, 파이썬은 메서드를 부분적으로 실행시키고, 

실행을 중단시키고, 그리고 스택 객체와 예외를 전역적으로 관리할 수 있는 방법이 필요할 것이다.

만약 파이썬이 익숙하다면, 제너레이터가 힌트가 될 수 있다는걸 깨달을 수 있을 것이다. 

제너레이터는 함수가 리스트를 리턴하는데 한 번에 하나의 아이템만 리턴할 수 있으며, 

다음 아이템이 필요할 때까지 실행이 중지된다. 

제너레이터의 문제점은 함수가 이를 호출해야만 수행될 수 있다는 것이다. 

즉, 제너레이터는 제너레이터를 호출할 수 없고, 서로의 실행을 중지시킬 수도 없다. 

그러나 이는 PEP 380이 제너레이터가 다른 제너레이터의 결과값을 yield할 수 있게 해주는 

yield from 문법을 추가할 때 까지만이다.

비동기가 정말 제너레이터의 의도는 아니지만, 

이는 비동기가 잘 돌아가는데에 필요한 모든 기능을 제공한다. 

제너레이터는 스택을 유지하며 예외를 발생시킬 수 있다. 

만약 당신이 제너레이터를 실행하는 이벤트 루프를 작성한다면, 

훌륭한 비동기 라이브러리를 가질 수 있다. 

그리고 따라서, asyncio 라이브러리가 탄생했다. 

당신이 해야 할 모든 것들은 @coroutine 데코레이터를 추가하는 것이며 

*asyncio*는 제너레이터를 코루틴 안으로 패치할 것이다. 

여기에 우리가 전에 봤듯이 세 개의 url을 호출하는 예시가 있다.

 

비동기 (Async)와 대기(Await)


asyncio 라이브러리는 많은 지지를 얻었고, 

파이썬은 이를 코어 라이브러리로 만들기로 결정하였다. 

코어 라이브러리의 도입과 함께, 

Python 3.5에는 async와 await 키워드 또한 추가되었다. 

이 키워드들은 코드가 비동기임을 더욱 명확하게 알 수 있도록 디자인 되었다. 

따라서 당신의 메서드가 제너레이터로 혼동되는 일이 없다. 

async 키워드는 메서드가 비동기임을 알 수 있도록 def 앞에 위치한다. 

await 키워드는 yield from 를 대신하며 

코루틴이 끝날때까지 대기하고 있음을 좀 더 명확하게 알 수 있다. 

여기에 async/await 키워드를 사용한 위와 똑같은 예시가 있다 



Python의 반복문


Python의 이런 반복은 얼핏보면 편하지만 함정이 있습니다. 

만약 0부터 1000억까지 반복을 해야한다면 어떨까요? 

range(100000000)이라고 적으면 0부터 1000억-1 만큼의 인자가 들어있는 

list가 생겨날 것입니다. list에 들어있는 것도 결국 컴퓨터가 저장하고 있어야 하는데 

이대로 가다간 메모리가 버틸까요? 이 방법으로 접근하면 너무나도 비효율적입니다. 

Python으론 이런 방법밖에 없는걸까요?


def gen():

    yield 'one'

    yield 'two'

    yield 'three'


g = gen()

print(next(g))  # one

print(next(g))  # two

print(next(g))  # three

print(next(g))  # raise StopIteration

yield는 함수 실행 중간에 빠져나올 수 있는 generator를 만들 때 사용합니다. return이었다면 'one'이 반환되고 끝났겠지만 실제로는 그 뒤로도 다시 사용할 수 있었죠.


yield는 단순히 값을 내보낼 수만 있는 것은 아니고, 넣어줄 수도 있습니다.


def gen():

    val = 111111

    while True:

        val = (yield val) * 111111


g = gen()

print(next(g))  # 111111

print(g.send(2))  # 222222

print(g.send(3))  # 333333


위에서도 언급했지만 대용량 자료 처리등은 메모리에 모두 올려놓고 할 수 없습니다. 

그런 경우 한 줄씩 읽은 뒤 generator를 이용한 반복처리를 하면 편합니다. 

실제로도 Flask에서의 대용량 파일 전송, Sphinx의 확장 개발등에 사용됩니다.


def gen1():

    for x in range(10):

        yield x

    for x in range(5):

        yield x



def gen2():

    yield from range(10)

    yield from range(5)



for a, b in zip(gen1(), gen2()):

    assert a == b

반복문을 써서 yield를 일일히 해주는 경우와 

yield from을 쓰는 경우의 차이점이 있다면 send로 값을 주고 받는 경우인데, 

send된 값은 가장 바깥의 yield로 전송됩니다.






파이썬은 asyncio 라는 훌륭한 비동기 프레임워크를 가지고 있다. 

이제 스레딩의 문제점들을 짚어보고 이들을 어떻게 해결했는지를 보자.


CPU 컨텍스트 스위칭 : asyncio 는 비동기이며 이벤트 루프를 사용한다. 

이는 I/O를 대기하는 동안 애플리케이션이 컨텍스트 스위치를 관리할 수 있도록 한다.

CPU 스위칭이 없다!


경쟁 조건 (Race Conditions) : asyncio 는 한 번에 오직 하나의 코루틴만 실행하며

정의된 지점에서만 스위칭이 일어나기 때문에, 코드는 경쟁 조건으로부터 안전하다.


데드락/라이브 잠금 (Dead-Locks/Live-Locks) : 

경쟁 조건에 대해 걱정할 필요가 없기 때문에, 잠금을 사용할 필요가 없다. 

이는 데드락으로부터 매우 안전하게 만들어준다. 

만약 두 개의 코루틴이 서로를 깨워야(wake) 할 필요가 있을 경우엔 여전히 데드락이 발생할 가능성이 있지만, 

이런 일을 해야할 경우는 매우 드물 것이다.

기아 상태 (Resource Starvation) : 모든 코루틴이 하나의 스레드에서 실행되고, 

추가적인 소켓이나 메모리를 필요로하지 않기때문에, 되려 리소스가 부족하기가 힘들 것이다. 

그러나 Asyncio 는 기본적인 스레드 풀인 “executor pool”을 하나 가지고 있다. 

만약 매우 많은 일들을 하나의 “executor pool”에서 실행한다면, 여전히 리소스 부족에 대한 문제가 발생할 수 있다. 

하지만, 매우 많은 실행 프로그램을 사용하는것은 안티 패턴이며, 아마 이런 일을 자주 하지는 않을 것이다.


공평하게도, asyncio 는 매우 훌륭하지만, 자체적인 문제점들을 가지고 있다. 

먼저, asyncio 는 파이썬의 새로운 개념이다. 몇 몇의 이상한 엣지 케이스들은 당신을 더욱 궁금한 상태로 만들 수도 있다.

 두번째로, 완전한 비동기를 구현하려고하면 모든 코드베이스가 비동기여야 한다. 

 모든 작은 코드 조각까지 하나하나 다. 

 이것은 동기(synchronous) 함수가 너무 많은 시간이 걸려 이벤트 루프를 블로킹할 수도 있기 때문이다. 

 asyncio를 위한 라이브러리는 여전히 초기 단계이기 때문에, 종종 당신의 스택의 일부분을 위한 비동기 버전을 찾기가 어렵다.

 



Comments