문법
def
키워드로 선언하는 모든 함수는 파이썬에서 기본적으로 동기 함수로 동작한다
아래와 같이 선언된 함수는 동기 함수로 작동한다
def do_sync() -> str:
return 'sync'
기존 def
키워드 앞에 async
키워드를 붙이면 이 함수는 비동기 처리되며, 이러한 비동기 함수를 파이썬에서는 코루틴(coroutine)이라고 부른다
async def do_async() -> str:
return 'async'
이러한 비동기 함수는 일반 동기 함수가 호출하듯이 호출하면 코루틴 객체가 반한됨
print(type(do_async()))
따라서 비동기 함수는 일반적으로 async
로 선언된 다른 비동기 함수 내에서 await
키워드를 붙여서 호출해야 함
async def main_async():
await do_async()
async
로 선언되지 않은 일반 동기 함수 내에서 비동기 함수를 호출하려면 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명의 사용자 데이터를 조회하는데 1초가 걸리고, 한 번에 여러 사용자의 데이터를 조회할 수 없다
- 각각 3명,2명,1명의 사용자 정보를 조회하는 요청 3개가 동시에 애플리케이션에 들어온다
동기 프로그래밍
사용자 데이터 조회를 동기 방식으로 처리해주는 find_users_sync
함수를 작성. 의도적으로 1초의 지연을 주기 위해 time.sleep()
함수 사용
import time
def find_users_sync(n: int) -> None:
for i in range(1 , n + 1):
print(f'{n}명 중 {i}번 째 사용자 조회 중')
time.sleep(1.0)
print(f'> 총 {n}명의 사용자 동기 조회 완료')
애플리케이션에 들어온 3개의 요청을 동기 처리하는 process_sync
함수 작성
def process_sync() -> None:
start = time.time()
find_users_sync(n=3)
find_users_sync(n=2)
find_users_sync(n=1)
end = time.time()
print(f'>>> 동기 처리 총 소요 시간 : {end - start}')
if __name__ == '__main__':
process_sync()
이 함수를 호출해보면 find_users_sync
함수가 총 6초 동안 3번 순차적으로 실행된다
3명 중 1번 째 사용자 조회 중
3명 중 2번 째 사용자 조회 중
3명 중 3번 째 사용자 조회 중
> 총 3명의 사용자 동기 조회 완료
2명 중 1번 째 사용자 조회 중
2명 중 2번 째 사용자 조회 중
> 총 2명의 사용자 동기 조회 완료
1명 중 1번 째 사용자 조회 중
> 총 1명의 사용자 동기 조회 완료
>>> 동기 처리 총 소요 시간 : 6.01879096031189
싱글 쓰레드 기반 웹 서버가 위와 같이 동작하면 실제 사용자는 오랫동안 지연을 경험해야 할 것이다.
비동기 프로그래밍
위에서 동기 처리되도록 작성된 코드를 파이썬의 async/await
키워드를 사용해서 비동기 처리될 수 있도록 개선해보자. 기존의 함수 선언에 async
키워드를 붙여서 일반 동기 함수가 아닌 비동기 함수(coroutine)로 변경하고 time.sleep()
함수 대신에 asyncio.sleep()
함수를 사용하여 1초의 지연을 발생시키자
time.sleep()
함수는 기다리는 동안 CPU를 그냥 놀게하는 바면, asyncio.sleep()
함수는 CPU가 놀지 않고 다른 처리를 할 수 있도록 해준다. 여기서 주의할 점은 asyncio.sleep()
자체도 비동기 함수이기 때문에 호출할 때 반드시 await
키워드를 붙여야 한다
import asyncio
async def find_users_async(n: int) -> None:
for i in range(1 , n + 1):
print(f'{n}명 중 {i}번 째 사용자 조회 중')
await asyncio.sleep(1.0)
print(f'> 총 {n}명 사용자 조회 완료')
이제 파이썬의 asyncio
모듈을 사용해서 위에서 작성한 함수를 비동기로 실행해보자. 먼저 이벤트 루프가 3개의 함수 호출을 알아서 스케줄하여 비동기로 호출할 수 있도록 asyncio.gather
함수의 인자로 3개의 함수 반환값, 즉 코루틴 객체를 넘겨주도록 수정하자
그리고 이렇게 수정된 process_async
비동기 함수를 호출할 때도, 함수의 반환값인 코루틴 객체를 asyncio.run
함수에 넘겨주도록 하자
async def process_sync() -> None:
start = time.time()
wait asyncio.gather(
find_users_sync(n=3)
find_users_sync(n=2)
find_users_sync(n=1)
)
end = time.time()
print(f'>>> 동기 처리 총 소요 시간 : {end - start}')
if __name__ == '__main__':
process_sync()
비동기 처리되도록 재작성된 코드를 실행해보면 호출 순서와 무방하게 실행 시간이 짧을 수록 먼저 처리되는 것을 알 수 있었다. 총 소요시간 또한 6초에서 3초로 단축되었음을 확인할 수 있다
실제 사용자 관점에서 3초가 걸리는 요청을 기다리지 않고, 1초가 걸리는 요청은 1초만에 응답이 오고 2초가 걸리는 요청은 2초 만에 응답이 올테니 매우 이상적이라고 할 수 있다
3명 중 1번 째 사용자 조회 중
2명 중 1번 째 사용자 조회 중
1명 중 1번 째 사용자 조회 중
3명 중 2번 째 사용자 조회 중
2명 중 2번 째 사용자 조회 중
> 총 1명의 사용자 동기 조회 완료
3명 중 3번 째 사용자 조회 중
> 총 2명의 사용자 동기 조회 완료
> 총 3명의 사용자 동기 조회 완료
>>> 비동기 처리 총 소요 시간 : 3.0035018920898438