Python/2.7 information

비동기 처리 관련.(python 3)

qkqhxla1 2017. 4. 30. 16:37

사실 비동기에 대해서는 자바스크립트의 promise()만 알고 있었고, 어느정도 흐름을 파악해서 다른걸 봐도 비슷할거라 생각했다. 파이썬 3.5부터 비동기 관련 문법이 새로 나왔는데(원래 모듈에 있었는데 기본 문법으로 들어갔다), 공부하려고 docs를 보고 예제를 봐도 잘 이해가 안되서 몇일동안 삽질했었다.


사실 아직도 완전히 이해가 된 건 아니지만, 일단 지금까지 이해한 내용을 기반으로 정리.(새로운 것을 깨닫거나 틀린 지식이 있을 경우 계속 바꿔나갈 예정)

사실 글을 쓰다가 발견한건데 쓸게 너무 많다.



아래의 모든 코드는 전부 아래의 참조 링크에서 가져왔습니다.

(문제될시 글 삭제하겠습니다.)



3.5이하버전에서는 코루틴 문법이 아래와 같았지만

# Python 3.4

import asyncio

@asyncio.coroutine
def greet(msg):
    yield from asyncio.sleep(1)
    prit(msg)

3.5이후로 코루틴 문법이 모듈에서 데코레이터로 가져오는게 아닌, 기본문법으로 추가되었다.

import asyncio

async def greet(msg):
    await asyncio.sleep(1)
    print(msg)

가장 기초적인 비동기인 코루틴을 출력하는 문법이다.

import asyncio

async def lazy_greet(msg):
    await asyncio.sleep(1)
    print(msg)

loop = asyncio.get_event_loop()
loop.run_until_complete(lazy_greet("hello"))
loop.close()

0. lazy_greet함수는 async키워드와 내부의 await로 코루틴으로 선언되어있다. 앞의 코루틴 글에서는 send()함수로 어떻게 동작하는지만 보여줬는데 async/await로 동일하게 구현이 가능하다. async/await이라고 async와 await를 한 키워드처럼 같이 붙여놓은 이유는 둘이 위의 형식으로 써야 비동기가 구현되는것 같아서이다. 참고 : http://stackabuse.com/python-async-await-tutorial/


await은 await뒤의 비동기 함수가 끝날때까지 기다려주는(blocking) 키워드이다. 

https://docs.python.org/3/whatsnew/3.5.html 의 중간쯤에 await를 읽어보면 awaitable한 객체일 경우에는 await키워드 뒤에 쓰는게 가능하다. 위의 코드에서 asyncio.sleep(1)가 쓰였는데 docs를 참고하면

이것도 코루틴이다. 그리고 await 뒤에 비동기로 처리할 작업을 넣는다.(아래 다시 언급.)


1. loop = asyncio.get_event_loop() 에서 현재 문맥에서의 이벤트 루프를 가져온다. 


2. run_until_complete함수로 비동기 함수들이 끝날때까지 실행시키고 close()에서 종료한다.


당연히 저 코드만 봐서는 await에 asyncio.sleep(1)이 있으므로 그냥 time.sleep(1)로 실행시키는 것과 다를게 없다. 하지만 여러개의 코루틴을 호출하는 경우에 진가가 드러난다.


복수개의 코루틴 호출 예제.

import asyncio
import random


async def lazy_greet(msg, delay=1):
    print(msg, "will be displayed in", delay, "seconds")
    await asyncio.sleep(delay)
    return msg.upper()


async def main():
    messages = ['hello', 'world', 'apple', 'banana', 'cherry']
    fts = [asyncio.ensure_future(lazy_greet(m, random.randrange(1, 5)))
           for m in messages]
    for f in asyncio.as_completed(fts):
        x = await f
        print(x)


loop = asyncio.get_event_loop()
loop.run_until_complete(main())
loop.close()

0. 이벤트 루프를 가져와서 main()을 실행한다.


1. main에서는 fts에 코루틴들을 넣어두는데, asyncio.ensure_future()로 감싸서 넣어준다. 이것은 코루틴을 받아서 Task를 리턴한다.(Future를 인자로 받으면 그냥 Future를 리턴한다. docs) asyncio.ensure_future에 대해 궁금증이 생겼었다. 위의 코드에서 asyncio.ensure_future을 감싸지 않고 fts를 만들어도 값은 똑같이 나온다. 그러면 이것의 역할은 뭐야??? 하고 스택오버플로우를 찾아본 결과 이 질문을 찾을수 있었다. http://stackoverflow.com/questions/34753401/difference-between-coroutine-and-future-task-in-python-3-5


ensur_future로 감싸지는 순간 실행될 순서가 스케쥴링된다고 한다. 간단하게는 결과값이 필요하면 이걸로 감싸주고, 아니면 굳이 감싸지 않아도 된다고 한다.


또 Task와 Future가 뭔지 설명이 필요하다. 

Future객체란 미래에 결과물을 내게 될 객체를 말한다. Task는 Future객체의 서브클래스로, 코루틴을 감싼다. 그래서 코루틴이 끝나게 되면 Task가 결과값을 얻게 된다. (아직 불확실한데 더 알게되면 추가)


2. asyncio.as_completed는 이름에서 알수있듯이 끝나는 순서대로 객체를 리턴한다. 구글링해서 이것저것 찾아봤는데 내부까진 잘 이해를 하진 못하겠는데, '끝나는 순서대로 객체를 리턴한다'기 보단 '객체를 리턴한뒤 끝나는 순서대로 할당된다'?? 인것 같다. await f부분에서 f가 끝나면 값을 리턴한다. 출력되는 값을 보면 나중에 실행되었더라도 초가 적으면 먼저 실행된다.


위 예제들의 주축 비동기부분은 전부 await asyncio.sleep(1)등으로 구현되어 있는데 이 코루틴이 비동기로 구현되어 있기 때문이다. 이것 말고 쓸만한 예제(여러 개의 사이트에서 비동기로 어떤 것들을 다운로드한다던가) 를 만들려면 aiohttp같은 비동기 요청 라이브러리를 이용해야만 할 것 같다. 

이것 말고, 사실 우리가 아는 일반적인 blocking인 함수들을 비동기로 돌리는게 가장 효율적인 방법 같다. 이에 대해서도 찾아봤다 : http://stackoverflow.com/questions/41063331/how-to-use-asyncio-with-existing-blocking-library

그리고 아래에 async + non blocking, async + blocking + multithread, sync + blocking의 속도 차이를 기록해놓았다.

# async + nonblocking function
import time
import datetime
import asyncio
import aiohttp

start = time.time()
url = ['https://www.python.org/', 'https://www.python.org/', 'https://www.python.org/',
          'https://www.python.org/', 'https://www.python.org/']

async def get(url):
    async with aiohttp.ClientSession() as session:
        async with session.get(url) as response:
            return await response.text()

async def main():
    tasks = [asyncio.ensure_future(get(u)) for u in url]
    for f in asyncio.as_completed(tasks):
        x = await f

loop = asyncio.get_event_loop()
loop.run_until_complete(main())
print('{1:<06.3f} {0}'.format(': async + nonblocking time',time.time() - start))


# async + blocking function + multithread
import concurrent.futures
import asyncio
import urllib.request
import time

start = time.time()
url = ["http://python.org","http://python.org","http://python.org",
       "http://python.org","http://python.org"]

def gethtml(url):
    urllib.request.urlopen(url)

async def non_blocking(loop, executor):
    await asyncio.wait(
        fs={
            loop.run_in_executor(executor, gethtml, u)
            for u in url
        },
        return_when=asyncio.ALL_COMPLETED
    )

loop = asyncio.get_event_loop()
executor = concurrent.futures.ThreadPoolExecutor(max_workers=len(url))
loop.run_until_complete(non_blocking(loop, executor))
print('{1:<06.3f} {0}'.format(': async + blocking + multithread time',time.time() - start))
loop.close()


# sync + blocking
import urllib.request
import time

start = time.time()
url = ["http://python.org","http://python.org","http://python.org",
       "http://python.org","http://python.org"]

for u in url:
    urllib.request.urlopen(u)
print('{1:<06.3f} {0}'.format(': sync + blocking time',time.time() - start))

뭔가 당연하지만, async + blocking에서는 쓰레드를 사용했다.(쓰레드를 사용하지 않으면 sync와 다를바가 없는듯..) max_workers에 사용할 최대 쓰레드의 갯수를 지정해서 비동기적으로 요청을 처리한다. 이런식으로 돌리면 될 것 같다. 결론. url 요청시에 비동기로 처리하려면 비동기 요청을 하자...(aiohttp같은) 


비동기로 blocking되는 함수를 호출하려면 쓰레드를 여러개 쓰는수밖에 없고 비동기의 장점을 극대화하려면 비동기 + nonblocking함수를 써야 한다.


위 소스코드를 돌려보면 async + nonblocking이 평균적으로 가장 빠른 결과를 보여준다. 네트워크 요청이니 네트워크 상황에 따라 달라질 때도 있지만 평균적으로 async가 빠른건 gil의 영향도 있는것같다.



또 이쯤되니 다시 이 개념들이 생각나서 아래 사이트를 찾았었다.

http://qnrdlqkrwhdgns.canxan.com/prob/post/250 예전에 이거 봤을때는 당연하지... 했는데 역시 코딩하니까 예전에 배웠던 개념들은 생각도 안나고 쩔쩔맸다. 공부를 더 열심히 해야겠다..



참고.(사실 더 많지만 많이 도움되었던 것만)

http://soooprmx.com/wp/archives/5625


http://soooprmx.com/wp/archives/6882


https://www.slideshare.net/deview/2d4python


http://lucumr.pocoo.org/2016/10/30/i-dont-understand-asyncio/


https://b.ssut.me/python-3-asyncio%EC%99%80-%EB%86%80%EC%95%84%EB%B3%B4%EA%B8%B0/


http://masnun.com/2015/11/20/python-asyncio-future-task-and-the-event-loop.html