Python 3.5부터 내장된 비동기 프로그래밍을 위한 라이브러리이다.
해당 라이브러리를 사용하게된 이유는 다량의 request 작업을 해야할 일이 있었는데 이 때 sync 방식으로 request를 하게 되면 너무 많은 시간을 잡아먹게 되어서 asyncio를 도입하게됐다.
급하게 도입을 하고 나서 다시 asyncio에 대해 정리하다보니 불필요하게 중복 사용되는 부분도 개선할 수 있었다.
Asyncio를 이해하기위해 필요한 용어를 정리하고 내가 사용한 asyncio 의 실행흐름을 간단하게 정리해본다.
async def foo():
__await__()
메소드가 구현된 Awaitable 객체라면 무엇이든지 사용 가능__await__()
메소드를 호출하여 제너레이터 객체를 얻고 이를 통해 해당 제너레이터를 실행하는 방식으로 동작.PENDING
, CANCELLED
, FINISHED
) 및 결과를 저장.FINISHED
상태가 된다.add_done_callback()
_coro
필드에 저장asyncio.run()
or asyncio.create_task()
함수를 호출할 때 인자로 코루틴 객체를 넘겨줘야 함.__step()
메소드가 호출되면 코루틴의 실행이 게시된다.python 버전은 3.6 버전을 기반으로 작성하여 asyncio.run() 함수와 asyncio.create_task() 함수를 사용하지 않았다. 추후 상위 버전 python을 쓰게되면 개선할 수 있을 듯!
async def bound_fetch(sem, url, session):
# Getter function with semaphore.
async with sem:
await fetch(url, session)
async def async_request(url_list):
"""
tasks = []
# create instance of Semaphore
sem = asyncio.Semaphore(MAX_THREAD)
conn = TCPConnector(limit=MAX_THREAD)
# Create client session that will ensure we dont open new connection
# per each request.
async with ClientSession(connector=conn, trust_env=True) as session:
tasks = [asyncio.ensure_future(bound_fetch(sem, DATAPACK_API_CONFIG_LIST["HOST"] + "/" + _url, session)) for _url in url_list] # Make Task(Future) object
responses = await asyncio.gather(*tasks) # Get data all at once
return responses
loop = asyncio.get_event_loop()
loop.run_until_complete(async_request(endpoint_list))
loop.close()
loop.run_until_complete()
함수에 의해 main_task 가 실행되고, 이로 인해 async_request()
코루틴이 실행된다.asyncio
에서 제공하는 Semaphore
를 이용하여 생성되는 request 를 제한하도록 한다. ( 서버가 감당 할 수 있을 양으로 컨트롤하기 위해 )ClientSession
을 활용하여 코루틴에서 session을 사용할 수 있다. async with
를 활용하여 비동기 컨텍스트 메니저를 사용할 수 있다. 아래 코드에서 보듯이 또 다른 코루틴을 만듦을 볼 수 있다.class AsyncContextManager:
async def __aenter__(self):
await log('entering context')
async def __aexit__(self, exc_type, exc, tb):
await log('exiting context')
async_request()
코루틴은 asyncio.ensure_future()
함수를 통해 복수의 task객체들을 생성하고 실행을 예약한다.asyncio.gather()
코루틴은 등록된 task 가 모두 완료될 때까지 이벤트 루프와 제어권을 주고 받으면서 확인한다.bound_fetch(sem,url,session)
코루틴은 Future 객체를 만들고, Future 객체의 결과 값이 갱신되도록 이벤트 루프에 예약을 건 뒤, Future 객체를 await 한다. asyncio.gather()
는 첫 테스크 등록때 그 테스크가 완료 상태가 될 때까지 await 중이 되는데 완료가 된다면 main_task 가 예약된다.좀 더 직관적으로 변경하면 아래와 같다.
import asyncio
import random
async def fetch(url):
print(f"Fetch: {url} started")
await asyncio.sleep(random.randint(1, 5))
print(f"Fetch: {url} ended")
# time.sleep(2)
async def bound_fetch(url):
# Getter function with semaphore.
print(f"Bound fetch {url} started")
await fetch(url)
print(f"Bound fetch {url} ended")
async def async_request(url_list):
tasks = []
tasks = [asyncio.ensure_future(bound_fetch(_url)) for _url in url_list] # Make Task(Future) object
responses = await asyncio.gather(*tasks) # Get data all at once
return responses
loop = asyncio.get_event_loop()
loop.run_until_complete(async_request(["https://naver.com", "https://yahoo.co.kr", "https://google.com"]))
loop.close()
순차적으로 제어권을 주고받으면서 작업이 진행되다 긴 시간의 I/O or Sleep 작업이 발생하고 event loop에서 누구에게도 할당할 수 없는 상황이 오게되고 그 때 event loop는 작업이 완료된 future객체를 기다리고 작업이 완료됐다면 가져와서 실행하게 되는 일련의 과정을 거치게 된다.