프로젝트를 하면서 비동기로 처리해야 할 일들이 생겼는데, 동기와 비동기에 대한 지식이 부족한 상태라 좀 알아봐야 할 것 같다.
먼저 사용되는 용어들 중에 정확하게 뜻을 모르는 단어들을 정리해봤다.
쓰레드(thread) : 어떠한 프로그램 내에서, 특히 프로세스 내에서 실행되는 흐름의 단위. 일반적으로 한 프로그램은 하나의 스레드를 가지고 있지만, 프로그램 환경에 따라 둘 이상의 스레드를 동시에 실행할 수 있다. 이런 실행 방식을 멀티스레드라고 한다.
동기와 비동기
동기 프로그래밍은 하나의 작업을 할 때는 그 작업에만 집중해서 해당 작업이 끝난 후에야 다음 작업을 실행할 수 있는 것을 말한다. 동기 프로그래밍 방식을 이용하면 어떠한 일을 처리할 동안 다른 프로그램은 정지한다. 반면, 비동기 프로그래밍은 요청을 보낸 후 응답(=결과)와는 상관없이 다음 작업이 동작하는 방식이다.
비동기가 사용되는 이유
전통적으로 동시 프로그래밍은 멀티 스레드를 이용하는 방식으로 이루어졌는데, thread safe한 프로그램을 작성하는 것은 쉬운 일이 아니라고 한다. 게다가 싱글 코어 프로세서에서 이런 프로그램을 돌리게 되면 오히려 성능이 저하되는 경우도 존재한다. 따라서 이를 보완하기 위해 요즘은 하나의 스레드 내에서 비동기 프로그래밍을 하는 방식으로 많이 사용된다고 한다.
파이썬을 이용한 비동기 프로그래밍
자바스크립트는 애초에 비동기 방식으로 동작하도록 설계된 언어라고 한다. 하지만 파이썬은 동기 방식으로 동작하는 것이 기본값이다. 비동기 프로그래밍의 활용도가 높아지면서 파이썬 3.5 버전 이상에서도 asyncio 라이브러리와 async / await 기본 키워드를 이용해서 언어 자체적으로 비동기 프로그래밍이 가능해졌다.
사용 문법과 예시
보통 파이썬에서 함수를 만들 때는 def를 이용한다. def를 사용하는 모든 함수는 동기식으로 사용된다고 이해하면 된다. 비동기식으로 만들고 싶은 함수에서는 async def 를 이용해준다.
async def a():
pass
a()
이렇게 async 를 이용해서 만들어준 함수를 파이썬에서는 코루틴(coroutine)이라고 부른다. 위의 코드를 실행해보면 다음과 같은 결과를 마주할 수 있다.
RuntimeWarning: coroutine 'a' was never awaited
a()
RuntimeWarning: Enable tracemalloc to get the object allocation traceback
첫 번째 RuntimeWarning에서 볼 수 있듯이 비동기 함수는 일반적으로 async로 선언된 다른 비동기 함수 내에서 await 키워드를 붙여서 호출해야 한다.
async def main_async():
await a()
이런 식으로 활용할 수 있다. 반면에 동기 함수에서 비동기 함수를 호출하고 싶을 수도 있는데, 이 때는 asyncio 라이브러리의 이벤트 루프를 이용해주면 된다.
import asyncio
loop = asyncio.get_event_loop()
loop.run_until_complete(main_async())
loop.close()
혹은 파이썬 3.7 버전 이상부터는 다음과 같은 코드로도 실행할 수 있다.
asyncio.run(main_async())
실습 예제
아래 적어둔 참고 블로그에서 배워볼 예제가 있길래 한번 따라해봤다.
사용자에 대한 데이터베이스를 가지고 있지 않고 외부 API를 통해 정보를 받아와야 한다고 해보자.
조건 :
- 외부 API는 1초에 한 명의 사용자에 대한 정보만을 가져올 수 있다.
- 한 번에 한 명의 사용자 정보만 가져올 수 있다.
- 각각 3명, 2명, 1명에 대한 정보에 대한 요청을 보낸다.
이를 동기 프로그래밍, 비동기 프로그래밍을 이용한 코드를 짜는 것이다.
먼저 동기 프로그래밍 방식은 다음과 같은 코드를 짰다.
import time
def find_users_sync(n):
for i in range(n):
print(f'{n}명 중 {i+1}번 째 사용자 조회 중 ...')
time.sleep(1)
print(f'> 총 {n}명 사용자 동기 조회 완료!')
def process_sync():
start = time.time()
find_users_sync(3)
find_users_sync(2)
find_users_sync(1)
end = time.time()
print(f'>>> 동기 처리 총 소요 시간 : {end-start}')
이 때 1초에 한 명의 사용자에 대한 요청만 가능하기 때문에 time.sleep(1)을 이용해주었다.
반면 비동기 프로그래밍 방식은 다음과 같이 짤 수 있다.
async def find_users_async(n):
for i in range(n):
print(f'{n}명 중 {i+1}번 째 사용자 조회 중 ...')
await asyncio.sleep(1)
print(f'> 총 {n}명 사용자 비동기 조회 완료!')
async def process_async():
start = time.time()
await asyncio.wait([
find_users_async(3),
find_users_async(2),
find_users_async(1),
])
end = time.time()
print(f'>>> 비동기 처리 총 소요 시간 : {end-start}')
우선 time.sleep(1) 대신 asyncio.sleep()을 이용해주었는데, 이렇게 하면 time.sleep()에서는 CPU 자체가 1초간 쉬었던 것과는 다르게 다른 처리를 할 수 있도록 도와준다. 이 때 asyncio.sleep() 자체도 비동기 함수기 때문에 호출하기에 앞서 await을 적어줘야 한다.
그리고는 process_async() 코루틴에서는 asyncio.wait을 이용해서 비동기식으로 처리될 함수들(coroutine 객체)의 리스트를 작성해줄 수 있다.
그리고 동기와 비동기를 차례대로 모두 실행해 보았다.
if __name__ == '__main__':
process_sync()
asyncio.run(process_async())
역시 여기서 실행할 때도 process_async()는 비동기식이니까 asyncio.run()을 이용하는 방식으로 동기 함수 내에서 비동기 함수를 호출해줬다.
비동기가 1초간 CPU를 멈추지 않고 다음 일을 먼저 수행하고 있을 수 있어서 훨씬 짧은 시간이 걸렸다. 다만, 비동기식으로 진행될 경우 어떤 것이 먼저 실행될 것인지에 대한 보장은 없다.
추가 내용
위의 코드를 실행하다 보니, 다음과 같은 문구를 만났다.
DeprecationWarning: The explicit passing of coroutine objects to asyncio.wait() is deprecated since Python 3.8, and scheduled for removal in Python 3.11.
대충 보니 3.8 이후 버전에서는 asyncio.wait()을 안 쓰는 게 좋고 3.11 버전부터는 아예 사라진다는 내용 같은데, 그럼 어떤 걸로 대체해서 써야할지 궁금해졌다. 공식 문서 내용을 확인해보자.
요약해보자면 asyncio.wait()은 타임아웃을 고려하지 않기 때문에 타임아웃이 발생했을 때, 동작을 취소하지 않는다. 따라서 timeout을 parameter로 가지는 asyncio.wait_for()을 사용하라고 한다.
바로 asyncio.wait_for()을 이용 가능하다면 좋겠지만, asyncio.wait_for()의 매개변수는 wait()과는 조금 다르다. wait()은 aws (awaitables)를 매개 변수로 가지므로 여러 테스크들(여러 동작들)을 한꺼번에 넣어줄 수 있었지만, wait_for()은 하나의 aw와 timeout값을 가진다. 그래서 별도의 함수를 사용해줘야 한다. asyncio.gather()을 이용하면 여러 task들을 하나의 task처럼 묶어줄 수 있다. 따라서 asyncio.wait_for()을 이용한 코드는 다음과 같아진다.
async def process_async():
start = time.time()
await asyncio.wait_for(asyncio.gather(
find_users_async(1),
find_users_async(2),
find_users_async(3)
), timeout=5.0)
end = time.time()
print(f'>>> 비동기 처리 총 소요 시간 : {end-start}')
위의 코드로 실행을 해보면, 다음과 같이 결과가 나온다! 순서는 내 맘대로 할 수 있는 게 아니라고는 했지만, find_users_async(3)부터 gather에 추가해주면 3명에 대한 요청부터 시작하길래 gather()에 넣는 순서를 바꿔봤더니 다음 결과를 받을 수 있었다. 타임아웃 값은 지금 실습해보는 과정에서는 3초만 넘으면 되니까 대략 5초로 설정해줬다. 만약 3초보다 짧으면 타임아웃 에러가 raise되니 주의하자!
'TIL > Python | Django' 카테고리의 다른 글
2021.12.3 TIL : [Django] nginx와 gunicorn (0) | 2021.12.04 |
---|---|
2021.11.26 TIL : [Django] ORM lazy loading (0) | 2021.11.27 |
2021.8.25 TIL : [Python] 웹 크롤러 - 2 (0) | 2021.08.24 |
2021.8.24 TIL : [Python] 웹 크롤러 - 1 (0) | 2021.08.24 |
2021.8.23 TIL : [Python] 기초 문법9 - super() (2) | 2021.08.23 |
댓글