-
파이썬 비동기 연습Python/이것저것 파이썬 2022. 8. 5. 13:52반응형
0. 먼저 이 영상을 보고 개념을..
자바스크립트로 예를 들었지만, 개념 설명이 좋은 것 같습니다...
1. 파이썬에서의 비동기를 잘 정리한 블로그..
https://kukuta.tistory.com/345
동기 프로그래밍을 비동기 프로그래밍으로 바꾸는 과정,
이 과정에서 필요한 키워드인
async, await, asyncio.run(), asyncio.gather(), asyncio.get_event_loop(), run_in_executor()
를 모두 알려주네요. 읽기 편합니다.- import asyncio
# 비동기 라이브러리 임포트 - def 대신 async def로 함수 선언
# 이렇게 선언된 함수를 (네이티브) 코루틴이라고 함. - await
# 비동기 함수의 결과를 기다려야 할 때 사용. - asyncio.gather()
# 여러 비동기 함수를 한번에 등록 - asyncio.run()
# 비동기 함수 실행 - asyncio.get_event_loop()
# 이벤트 루프 만듦 - [루프].run_in_executor()
# 동기 함수를 [루프]에 태워 (비동기적으로 실행)
원글의 코드는 다음과 같습니다..
import time import requests import asyncio # asyncio 모듈 임포트 async def download_page(url): # async def로 함수 정의 loop = asyncio.get_event_loop() # 이벤트 루프 객체 얻기 req = await loop.run_in_executor(None, requests.get, url) # 동기함수를 비동기로 호출 html = req.text print("complete download:", url, ", size of page(", len(html), ")") async def main(): await asyncio.gather( download_page("https://www.python.org/"), download_page("https://www.python.org/"), download_page("https://www.python.org/"), download_page("https://www.python.org/"), download_page("https://www.python.org/") ) print(f"stated at {time.strftime('%X')}") start_time = time.time() asyncio.run(main()) finish_time = time.time() print(f"finish at {time.strftime('%X')}, total:{finish_time - start_time} sec(s)")
파이썬의 공식 문서들을 보면서 이것저것 만져봅니다.
https://docs.python.org/ko/3/library/asyncio.html
https://docs.python.org/ko/3/library/asyncio-api-index.html
https://docs.python.org/ko/3/library/asyncio-task.html
초심자답게 고수준 API 위주로 만져 봅시다.
3. 연습
연습 삼아 조금 수정해보았습니다.
내장 라이브러리를 사용하는 게 편할 것 같았고.
루프 선언을 하나로 모으는 것도 가능할 것 같아서...asyncio.all_tasks()를 이용해서 완료되지 않은 태스크를 확인해 봅니다.import time from urllib import request # import asyncio def req(url): with request.urlopen(url) as response: html = response.read() return html async def download_page(loop, url): # print(len(asyncio.all_tasks())) html = await loop.run_in_executor(None, req, url) print("complete download:", url, ", size of page(", len(html), ")") # print(len(asyncio.all_tasks())) async def main(): loop = asyncio.get_event_loop() # await asyncio.gather( download_page(loop, "https://www.python.org/"), download_page(loop, "https://www.python.org/"), download_page(loop, "https://www.python.org/"), download_page(loop, "https://www.python.org/"), download_page(loop, "https://www.python.org/") ) print('start') start_time = time.time() asyncio.run(main()) print(f'finish, total:{time.time() - start_time} sec(s)')
run_in_executor의 첫 번째 인수는 executor인데 함수를 실행시켜줄 스레드 풀 또는 프로세스 풀입니다.
None을 넣으면 기본 스레드 풀을 사용합니다.
두 번째 인수는 실행할 함수, 세 번째 인수부터는 실행할 함수에 들어갈 인수입니다.start complete download: https://www.python.org/ , size of page( 50024 ) complete download: https://www.python.org/ , size of page( 50024 ) complete download: https://www.python.org/ , size of page( 50024 ) complete download: https://www.python.org/ , size of page( 50024 ) complete download: https://www.python.org/ , size of page( 50024 ) finish, total:0.17750239372253418 sec(s)
잘 작동하네요.
0.17초
동기식
import time from urllib import request def req(url): with request.urlopen(url) as response: html = response.read() return html def download_page(url): html = req(url) print("complete download:", url, ", size of page(", len(html), ")") def main(): download_page("https://www.python.org/"), download_page("https://www.python.org/"), download_page("https://www.python.org/"), download_page("https://www.python.org/"), download_page("https://www.python.org/") print('start') start_time = time.time() main() print(f'finish, total:{time.time() - start_time} sec(s)')
start complete download: https://www.python.org/ , size of page( 50024 ) complete download: https://www.python.org/ , size of page( 50024 ) complete download: https://www.python.org/ , size of page( 50024 ) complete download: https://www.python.org/ , size of page( 50024 ) complete download: https://www.python.org/ , size of page( 50024 ) finish, total:0.33704614639282227 sec(s)
동기식은 확실히 느리죠? 0.33초
참고) asyncio.run 예전 문법
# 파이썬 3.7 이후에 포함된 문법 asyncio.run(main()) # 파이썬 3.6까지 loop = asyncio.get_event_loop() loop.run_until_complete(main()) loop.close()
asyncio.get_event_loop()는 파이썬 3.10에서 경고가 발생합니다.
get_event_loop 대신 new_event_loop을 사용하면 됩니다.파이썬 공홈에 가보니 버전 3.10부터 get_event_loop는 폐지라고 되어 있네요.
실행 중인 이벤트 루프가 없다면 디프리케이션 경고가 발생합니다.
미래의 파이썬 릴리즈에서 get_event_loop는
get_running_loop의 alias(가명)가 될 것입니다.Deprecation warning is emitted if there is no running event loop.
In future Python releases, this function will be an alias of get_running_loop().예전 문법을 이용해서 이렇게 작성해도 될 것 같아서...
import time from urllib import request import asyncio def req(url): with request.urlopen(url) as response: html = response.read() return html async def download_page(url): # print(len(asyncio.all_tasks())) html = await loop.run_in_executor(None, req, url) print("complete download:", url, ", size of page(", len(html), ")") # print(len(asyncio.all_tasks())) async def main(): await asyncio.gather( download_page("https://www.python.org/"), download_page("https://www.python.org/"), download_page("https://www.python.org/"), download_page("https://www.python.org/"), download_page("https://www.python.org/"), ) print('start') start_time = time.time() # asyncio.run(main()) # loop = asyncio.get_event_loop() loop = asyncio.new_event_loop() loop.run_until_complete(main()) loop.close() print(f'finish, total:{time.time() - start_time} sec(s)')
비동기로 잘 작동합니다.
asyncio.create_task()
코루틴을 Task로 감싸고 실행을 '예약'합니다. Task 객체를 반환합니다.
라고 공식 홈페이지에 되어 있네요.(제가 비동기 프로그램을 많이 해보지 못해) 이 부분은 좀 모호합니다.
비동기가 안될 것 같은 느낌인데 비동기가 되네요.이렇게 예약을 해두면 게더로 모을 필요가 없는 것 같습니다.import time from urllib import request import asyncio def req(url): with request.urlopen(url) as response: html = response.read() return html async def download_page(url): # loop = asyncio.get_event_loop() loop = asyncio.get_running_loop() html = await loop.run_in_executor(None, req, url) print("complete download:", url, ", size of page(", len(html), ")") async def main(): task1 = asyncio.create_task(download_page("https://www.python.org/")) task2 = asyncio.create_task(download_page("https://www.python.org/")) task3 = asyncio.create_task(download_page("https://www.python.org/")) task4 = asyncio.create_task(download_page("https://www.python.org/")) task5 = asyncio.create_task(download_page("https://www.python.org/")) # print(asyncio.all_tasks()) await task1 await task2 await task3 await task4 await task5 # print(asyncio.all_tasks()) print('start') start_time = time.time() asyncio.run(main()) print(f'finish, total:{time.time() - start_time} sec(s)')
이렇게 작성하면, 위에서 보았던 디프리케이션 경고가 발생하지 않습니다.
파이썬 3.10부터는 asyncio.run()으로 새 이벤트 루프를 선언하고,
get_running_loop() [=get_event_loop()]로
실행 중인 이벤트 루프를 가져온다는 느낌으로 코드를 작성할 수 있을 것 같습니다.이 게시물 작성하면서 처음 한 일이 루프 선언(get_event_loop())을 하나로 모으는 것이었습니다.
실제로 작동되는 루프는 1개인데, 루프 선언이 여러 번 반복되는 점이 보기 불편했습니다.all_tasks()로 확인하기 전까지는 선언할 때마다 새로운 루프가 생성되나 라는 생각도 잠시 했었습니다.파이썬답게 모호한 부분들이 버전 업되면서 명확하게 개선되는군요.
create_task는 모호한 것 같은데.... ㅜㅜstart complete download: https://www.python.org/ , size of page( 50108 ) complete download: https://www.python.org/ , size of page( 50108 ) complete download: https://www.python.org/ , size of page( 50108 ) complete download: https://www.python.org/ , size of page( 50108 ) complete download: https://www.python.org/ , size of page( 50108 ) finish, total:0.17334628105163574 sec(s)
take1~5까지의 변수로 Task 객체를 받아주지 않고
바로 await를 걸어 보았습니다.
동기적으로 작동하네요.이 부분은 파이썬 답지 않게 조금 모호한 느낌입니다.
import time from urllib import request import asyncio def req(url): with request.urlopen(url) as response: html = response.read() return html async def download_page(url): loop = asyncio.get_event_loop() html = await loop.run_in_executor(None, req, url) print("complete download:", url, ", size of page(", len(html), ")") async def main(): await asyncio.create_task(download_page("https://www.python.org/")) # print(asyncio.all_tasks()) await asyncio.create_task(download_page("https://www.python.org/")) # print(asyncio.all_tasks()) await asyncio.create_task(download_page("https://www.python.org/")) # print(asyncio.all_tasks()) await asyncio.create_task(download_page("https://www.python.org/")) # print(asyncio.all_tasks()) await asyncio.create_task(download_page("https://www.python.org/")) # print(asyncio.all_tasks()) print('start') start_time = time.time() asyncio.run(main()) print(f'finish, total:{time.time() - start_time} sec(s)')
start complete download: https://www.python.org/ , size of page( 50108 ) complete download: https://www.python.org/ , size of page( 50108 ) complete download: https://www.python.org/ , size of page( 50108 ) complete download: https://www.python.org/ , size of page( 50108 ) complete download: https://www.python.org/ , size of page( 50108 ) finish, total:0.3019280433654785 sec(s)
이렇게 해매던 중 이 글에서 힌트를 얻었습니다.
asyncio.create_task는 왜 쓰는 걸까?
이 함수의 역할은, 파라미터로 들어오는 코루틴을 Eventloop에 등록하고 코루틴이 끝났을 때 결과를 받아볼 수 있는 Future 객체를 반환합니다. 반환되는 Future 또한 Awaitable 객체이기 때문에 await을 앞에 붙이면 Eventloop에 실행권을 넘기면서 코루틴의 종료까지 기다릴 수 있지만, 지금은 필요하지 않기 때문에 의도적으로 await 없이 넘어갔습니다. 그 때문에 실행권을 뺏기지 않은 채로 다음 while iteration을 위해 10초간 기다릴 수 있습니다.
사실 asyncio.create_task()가 반환하는 것은 Task 객체이고, 이 객체는 Future를 상속받기 때문에 동일하게 Awaitable합니다.반응형 - import asyncio