사실 비동기에 대해서는 자바스크립트의 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
'Python > 2.7 information' 카테고리의 다른 글
multiprocessing에서 여러 프로세스가 동시 변수 참조하는 문제 (2) | 2017.05.29 |
---|---|
파이썬 정규식 한글. (0) | 2017.05.12 |
yield from, generator(yield) vs coroutine (python 3) (0) | 2017.04.26 |
mongo db (0) | 2017.04.19 |
로그 찍기. (0) | 2017.03.30 |