파이썬 비동기란?
Javascript에서처럼 비동기 작동을 하는 것과 같은 동작을 asyncio를 통해 가능합니다.
동시 프로그래밍 패러다임의 변화
전통적으로 동시 프로그래밍(concurrent programming)은 여러 개의 쓰레드(thread)를 활용하여 이루어졌었습니다. 하지만 thread safe한 프로그램을 작성하는 것은 생각보다 쉬운 일이 아닙니다. 더불어 싱글 코어 프로세서에서 이러한 프로그램을 작동시키면, 동시 처리에 따른 성능 저하를 일으키거나 미비합니다.
이에 따라 최근에는 하나의 쓰레드로 동시 처리를 하는 비동기 프로그래밍(asynchronous programming)이 많이 활용되고 있습니다.
비동기 프로그래밍
웹 서버와 같은 애플리케이션을 생각해보면 CPU 연산 시간 대비 DB나 API와 연동 과정에서 발생하는 대기 시간이 훨씬 길다는 것을 알 수 있습니다. 비동기 프로그래밍은 이러한 대기 시간을 낭비하지 않고 그 시간에 CPU가 다른 처리를 할 수 있도록 하는데 이를 흔히 non-blocking하다고 합니다.
Javascript와 같이 애초에 비동기 방식으로 동작하도록 설계된 언어에서는 익숙한 개념이지만, 파이썬과 같이 기본적으로 동기 방식으로 동작하는 언어에서는 이 기념이 생소하게 느껴질 수도 있습니다. 하지만 파이썬 3.4에서 asyncio가 표준 라이브러리로 추가되고, 파이썬 3.5에서 async/await 키워드가 문법으로 채택이 되면서, 파이썬도 이제 언어 자체적으로 비동기 프로그래밍이 가능해졌습니다.
문법
def 키워드로 선언하는 모든 함수는 파이썬에서 기본적으로 동기 방식으로 동작합니다.
ex)
def do_sync():
pass
기존 def 키워드 앞에 async 키워드까지 붙이면 이 함수는 비동기 처리되며, 이러한 비동기 함수를 파이썬에서는 코루틴(coroutine)이라고도 부릅니다.
ex)
async def do_async():
pass
이러한 비동기 함수는 일반 동기 함수가 호출하듯이 호출하면 coroutine 객체가 리턴됩니다.
따라서 비동기 함수는 일반적으로 async로 선언된 다른 비동기 함수 내에서 await 키워드를 붙여서 호출해야 합니다.
ex)
async def main_async():
await do_async()
* 자바스크립트에서 async로 선언된 비동기 함수를 호출할 때 await 키워드를 붙이지 않으면 Promise 객체를 리턴하는 것과 같은 것입니다.
async로 선언되지 않은 일반 동기 함수 내에서 비동기 함수를 호출하려면 asyncio 라이브러리의 이벤트 루프를 이용해야합니다.
loop = asyncio.get_event_loop()
loop.run_until_complete(main_async())
loop.close()
파이썬 3.7 이상에서는 아래와 같이 한 줄로 간단히 비동기 함수를 호출 할 수도 있습니다.
asyncio.run(main_async())
실습
사용자 관리 애플리케이션을 흉내내는 실습 코드를 작성하면서 동기 처리하는 코드와 비동기 처리를 하는 코드를 비교해보도록 하겠습니다.
시뮬레이션을 위해서 다음과 같은 가정을 해보겠습니다.(상황을 가정한 것입니다.)
- 애플리케이션을 사용자 데이터를 직접 보관하지 않고 외부 API를 호출해서 가져옵니다.
- 외부 API는 1명의 사용자 데이터를 조회하는데 1초가 걸리고, 한 번에 여러 사용자의 데이터를 조회할 수 없습니다.
- 각각 3명, 2명, 1명의 사용자 정보를 조회하는 요청 3개가 동시에 애플리케이션에 들어옵니다.
동기 프로그래밍
먼저 사용자 데이터 조회를 전통적인 동기 방식으로 처리해주는 do_sync 함수를 작성합니다. 의도적으로 1초의 지연 시간을 발생시키기 위해서 time.sleep 함수를 사용하였습니다.
import time
def do_sync(n):
for i in range(1, n + 1):
print(f'{n}명 중 {i}번 째 사용자 조회 중 ...')
time.sleep(1)
print(f'> 총 {n} 명 사용자 동기 조회 완료!')
그 다음, 애플리케이션에 들어온 3개의 요청을 동기 처리하는 doing_process_sync 함수를 작성합니다.
def doing_process_sync():
start = time.time()
do_sync(3)
do_sync(2)
do_sync(1)
end = time.time()
print(f'>>> 동기 처리 총 소요 시간: {end - start}')
if __name__ == '__main__':
doing_process_sync()
이 함수를 호출해보면 do_sync 함수가 총 6초 동안 3번 순차적으로 실행됨을 알 수 있습니다.
3명 중 1번 째 사용자 조회 중 ...
3명 중 2번 째 사용자 조회 중 ...
3명 중 3번 째 사용자 조회 중 ...
> 총 3 명 사용자 동기 조회 완료!
2명 중 1번 째 사용자 조회 중 ...
2명 중 2번 째 사용자 조회 중 ...
> 총 2 명 사용자 동기 조회 완료!
1명 중 1번 째 사용자 조회 중 ...
> 총 1 명 사용자 동기 조회 완료!
>>> 동기 처리 총 소요 시간: 6.315598726272583
만약에 싱글 쓰레드의 웹 서버가 이러한 방식으로 동작한다면 실제 사용자는 얼마나 오랫동안 지연을 경험을 하게 될까요? 동기 처리에서는 첫 번째 함수의 실행이 끝나야 두 번째 함수가 실행되고, 마찬가지로 두 번째 함수가 끝나야 세 번째 함수가 실행됩니다. 즉, 첫 번쨰 요청이 처리되는데는 3초, 두 번째 요청은 5초(3 + 2), 세 번째 요청은 6초(3 + 2 + 1)가 걸릴 것입니다.
비동기 프로그래밍
위에서 동기 처리되도록 작성된 코드를 파이썬의 async/await 키워드를 사용해서 한 번 비동기 처리될 수 있도록 개선해보도록 하겠습니다. 기존의 함수 선언에 async 키워드를 붙여서 일반 동기 함수가 아닌 비동기 함수(coroutine)로 변경하였으며, time.sleep 함수 대신에 asyncio.sleep 함수를 사용하여 1초의 지연을 발생시켰습니다.
time.sleep 함수는 기다리는 동안 CPU를 그냥 놀고있는 반면에, asyncio.sleep 함수는 CPU가 놀지 않고 다른 처리를 할 수 있도록 해줍니다. 여기서 주의할 점은 asyncio.sleep 자체도 비동기 함수이기 때문에 호출할 때 반드시 await 키워드를 붙여야 한다는 것입니다.
import time
import asyncio
async def do_async(n):
for i in range(1, n + 1):
print(f'{n}명 중 {i}번 째 사용자 조회 중 ...')
await asyncio.sleep(1)
print(f'> 총 {n} 명 사용자 비동기 조회 완료!')
자 이제, 파이썬의 asyncio 라이브러리를 사용해서 위에서 작성한 함수를 비동기로 실행해보겠습니다. 먼저 이벤트 루프가 3개의 함수 호출을 알아서 스케줄하여 비동기로 호출할 수 있도록 asyncio.wait 함수의 배열 인자로 3개의 함수 리턴값, 즉 coroutine 객체를 넘겨주도록 수정합니다. 그리고 이렇게 수정된 process_async 비동기 함수를 호출할 때도, 함수의 리턴값인 coroutine 객체를, asyncio.run 함수에 넘겨줍니다.
async def doing_process_async():
start = time.time()
await asyncio.wait([
do_async(3),
do_async(2),
do_async(1),
])
end = time.time()
print(f'>>> 비동기 처리 총 소요 시간: {end - start}')
if __name__ == '__main__':
asyncio.run(doing_process_async())
비동기 처리되도록 재작성된 코드를 실행해보면 호출 순서와 무방하게 실행 시간이 짧은 수록 먼저 처리되는 것을 알 수 있습니다. 게다가 총 소요 시간도 6초에서 3초로 절반만큼의 시간으로 단축되었음을 알 수 있습니다.
실제 사용자 관점에서 생각해보면 3초가 걸리는 요청을 기다리지 않고, 1초가 걸리는 요청은 1초 만에 응답이 오고, 2초가 걸리는 요청은 2초 만에 응답이 올테니 매우 이상적이지 않을 수 없습니다.
1명 중 1번 째 사용자 조회 중 ...
2명 중 1번 째 사용자 조회 중 ...
3명 중 1번 째 사용자 조회 중 ...
> 총 1 명 사용자 비동기 조회 완료!
2명 중 2번 째 사용자 조회 중 ...
3명 중 2번 째 사용자 조회 중 ...
> 총 2 명 사용자 비동기 조회 완료!
3명 중 3번 째 사용자 조회 중 ...
> 총 3 명 사용자 비동기 조회 완료!
>>> 비동기 처리 총 소요 시간: 3.1606404781341553
기본적으로 비동기 처리는 정확히 실행 순서가 보장되지 않기 때문에, 여러분 PC에서 실행했을 때는 저와 약간 실행 순서가 다를 수도 있습니다. 비록 동일한 실행 순서를 보장받지 못하더라도, 여기서 중요한 점은 CPU를 놀리지 않고 불필요한 지연없이 3개의 요청이 실행되어야 한다는 것입니다.
'👨🏫Study > Python' 카테고리의 다른 글
[Python] FastAPI 학습 (0) | 2021.08.12 |
---|---|
[Python] FastAPI 시작 (0) | 2021.08.08 |
댓글