비동기, async, await, asyncio, 코루틴, 비동기 함수 디버깅...
코드 리뷰를 하는데 리턴값이 수상하다!
분명 list 를 리턴해야 하는데, 리턴값은 웬 object 이고
object 안에는 cr_await, cr_code 와 같은 처음 보는 속성들이 들어 있다.
사수님께 여쭤보니 비동기 함수의 리턴값이라 그렇다며 코루틴(coroutine) 이라는 용어도 알려주셨는데
이번 기회에 비동기 개념 다시 한 번 정리하고 코루틴이 뭔지 알아보자! 🔥
빠르게 알아보기 위해 블로그 구글링과 ChatGPT 를 이용하였으며
ChatGPT 는 2023년 6월 13일 기준 3.5버전에 물어보았다. 🤖
비동기 프로그래밍은 작업의 실행과 완료를 기다리지 않고 동시에 여러 작업을 처리하는 프로그래밍 패러다임입니다. 전통적인 동기 프로그래밍에서는 작업이 순차적으로 실행되며, 각 작업의 완료를 기다려야 다음 작업을 수행할 수 있습니다. 반면에 비동기 프로그래밍은 여러 작업을 동시에 시작하고, 작업이 완료되는 시점을 기다리지 않고 다른 작업을 처리할 수 있습니다.
비동기 프로그래밍에서는 비동기 작업을 수행하는 함수나 메서드를 호출하고, 해당 작업이 완료될 때까지 기다리지 않고 다른 작업을 수행합니다. 작업이 완료되면 알림이 전달되거나 콜백 함수가 호출되어 작업의 결과를 처리할 수 있습니다. 이런 방식으로 여러 작업을 동시에 처리하고, 작업이 완료되는 시점에 결과를 처리함으로써 프로그램의 응답성과 성능을 향상시킬 수 있습니다.
비동기 프로그래밍은 일반적으로 이벤트 기반(event-driven)이나 콜백(callback) 기반으로 구현됩니다. 이벤트 기반 비동기 프로그래밍에서는 이벤트 루프(event loop)를 사용하여 여러 이벤트를 처리하고, 콜백 기반 비동기 프로그래밍에서는 작업이 완료되었을 때 호출되는 콜백 함수를 정의하여 작업의 결과를 처리합니다. 최근에는 async
/await
문법을 활용한 코루틴 기반의 비동기 프로그래밍도 많이 사용됩니다.
비동기 프로그래밍은 주로 I/O 작업이 많은 경우나 여러 작업을 동시에 처리해야 하는 경우에 특히 유용합니다. 네트워크 통신, 웹 개발, 데이터베이스 액세스, 파일 입출력 등의 작업은 대부분 I/O 작업이므로 비동기 방식으로 처리하는 것이 효과적입니다.
비동기 프로그래밍은 다음과 같은 이유로 많이 사용됩니다:
응답성(Responsiveness): 비동기 작업은 여러 작업을 동시에 처리하고, 작업이 완료되기를 기다리는 동안 다른 작업을 처리할 수 있습니다. 이로써 프로그램의 응답성이 향상되고, 장기 실행 작업이 다른 작업을 차단하지 않도록 할 수 있습니다. 예를 들어, 네트워크 요청이나 데이터베이스 조회 등의 I/O 작업을 비동기로 처리하면, 다른 작업을 수행하면서 응답을 기다릴 필요가 없어집니다.
확장성(Scalability): 비동기 프로그래밍은 많은 수의 동시 요청 또는 작업을 처리할 때 유용합니다. 여러 작업을 동시에 실행하고 완료될 때까지 기다리지 않아도 되므로, 처리량과 처리 속도를 향상시킬 수 있습니다. 이는 웹 서버, 네트워크 서비스, 데이터 파이프라인 등과 같은 시스템의 확장성을 향상시키는 데 도움이 됩니다.
자원 효율성(Resource Efficiency): 비동기 작업은 작업의 대기 시간 동안 자원을 효율적으로 활용할 수 있습니다. 대기 시간 동안 CPU나 메모리 등의 자원을 다른 작업에 할당하여 더 많은 작업을 처리할 수 있습니다. 이는 시스템의 자원 이용률을 높이고, 전체적인 성능을 향상시킬 수 있습니다.
병렬성(Concurrency): 비동기 프로그래밍은 병렬적인 작업을 효율적으로 처리할 수 있는 방법을 제공합니다. 여러 작업이 동시에 실행될 수 있으며, 이는 멀티코어 프로세서를 활용하여 작업을 분산 처리할 수 있음을 의미합니다. 이를 통해 병렬성을 높여 전체적인 처리 시간을 단축시킬 수 있습니다.
(출처 : https://www.daleseo.com/python-asyncio/)
Javascript 를 다뤄 보았다면 async
/ await
에 익숙할 텐데요,
파이썬에도 async
/ await
이 있고, asyncio
라이브러리를 사용합니다.
def 키워드로 선언하는 모든 함수는 파이썬에서 기본적으로 동기 방식으로 동작한다고 생각하시면 됩니다.
예를 들어, 다음과 같이 선언된 함수는 동기 함수입니다.
def do_sync():
pass
기존 def 키워드 앞에 async
키워드까지 붙이면 이 함수는 비동기 처리되며, 이러한 비동기 함수를 파이썬에서는 코루틴(coroutine)이라고도 부릅니다.
async def do_async():
pass
이러한 비동기 함수는 일반 동기 함수가 호출하듯이 호출하면 coroutine 객체가 리턴됩니다.
do_async() # <coroutine object do_async at 0x1038de710>
따라서 비동기 함수는 일반적으로 async
로 선언된 다른 비동기 함수 내에서 await
키워드를 붙여서 호출해야 합니다.
(await
는 비동기 함수나 코루틴의 실행을 일시 중단하고 다른 작업을 처리할 수 있도록 합니다.)
async def main_async():
await do_async()
async
로 선언되지 않은 일반 동기 함수 내에서 비동기 함수를 호출하려면 asyncio.run
을 이용합니다.
asyncio.run(main_async())
(출처 : https://dojang.io/mod/page/view.php?id=2418)
일반적으로 메인 함수는 서브 루틴을 호출한 뒤 서브 루틴의 작업이 끝날 때까지 기다립니다.
만약 서브 루틴이 파일 I/O 내지 대용량 파일 다운로드와 같은 작업을 수행한다면
우리는 서브 루틴이 작업을 마칠 때까지 기다려야 하죠.
https://dojang.io/mod/page/view.php?id=2418
이 기다림을 코루틴 (Coroutine : Cooperative routine) 이 보완해 주는데요,
메인 루틴과 서브 루틴처럼 종속된 관계가 아니라 서로 대등한 관계이며 특정 시점에 상대방의 코드를 실행합니다.
이처럼 코루틴은 함수가 종료되지 않은 상태에서 메인 루틴의 코드를 실행한 뒤 다시 돌아와서 코루틴의 코드를 실행합니다. 따라서 코루틴이 종료되지 않았으므로 코루틴의 내용도 계속 유지됩니다.
일반 함수를 호출하면 코드를 한 번만 실행할 수 있지만, 코루틴은 코드를 여러 번 실행할 수 있습니다.
동기 - 비동기 차이점을 코드와 수행시간 비교를 통해 알아봅시다.
(예제 코드 출처 : https://www.daleseo.com/python-asyncio/)
find_users_sync(n) 함수가 순차적으로 실행됩니다.
import time
def find_users_sync(n):
for i in range(1, n + 1):
print(f'{n}명 중 {i}번 째 사용자 조회 중 ...')
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}')
if __name__ == '__main__':
process_sync()
실행 결과
3명 중 1번 째 사용자 조회 중 ...
3명 중 2번 째 사용자 조회 중 ...
3명 중 3번 째 사용자 조회 중 ...
> 총 3 명 사용자 동기 조회 완료!
2명 중 1번 째 사용자 조회 중 ...
2명 중 2번 째 사용자 조회 중 ...
> 총 2 명 사용자 동기 조회 완료!
1명 중 1번 째 사용자 조회 중 ...
> 총 1 명 사용자 동기 조회 완료!
>>> 동기 처리 총 소요 시간: 6.021384954452515
import time
import asyncio
# async -> 비동기 함수로 변경
async def find_users_async(n):
for i in range(1, n + 1):
print(f'{n}명 중 {i}번 째 사용자 조회 중 ...')
# asyncio.sleep 이 비동기 함수이므로 await 추가
await asyncio.sleep(1)
print(f'> 총 {n} 명 사용자 비동기 조회 완료!')
# async -> 비동기 함수로 변경
async def process_async():
start = time.time()
# asyncio.wait : 함수 호출을 알아서 스케줄링하여 비동기로 호출
await asyncio.wait([
# asyncio 모듈의 wait() 함수에 넘겨지는 coroutines(코루틴)이 명시적으로 tasks(태스크)로 전달되어야 하므로
# asyncio.create_task() 함수를 사용하여 각 코루틴을 태스크로 변환해야 합니다.
# 태스크 : 코루틴을 동시에 예약하는 데 사용.
# 코루틴이 asyncio.create_task()와 같은 함수를 사용하여 태스크로 싸일 때 코루틴은 곧 실행되도록 자동으로 예약됨.
asyncio.create_task(find_users_async(3)),
asyncio.create_task(find_users_async(2)),
asyncio.create_task(find_users_async(1)),
])
end = time.time()
print(f'>>> 비동기 처리 총 소요 시간: {end - start}')
if __name__ == '__main__':
asyncio.run(process_async())
주석을 다시 정리하면,
1. 모든 함수 앞에 async
를 붙여서 비동기 함수로 바꿔 주고
2. 비동기 함수를 호출할 때 함수 앞에 await
를 붙여 주고
3. 코루틴으로 실행할 함수는 asyncio.create_task() 로 감싸서 task
로 변환해 주고
4. task 들을 asyncio.wait([task1, task2...])
에 넣어서 알아서 스케줄링되어 비동기적으로 호출되게 하고
5. 메인에서 asyncio.run()
를 이용해 가장 먼저 실행되어야 할 비동기 함수를 호출합니다.
(async 로 선언되지 않은 일반 동기 함수, 즉 메인 함수 내에서 비동기 함수를 호출하기 위해.)
실행 결과
3명 중 1번 째 사용자 조회 중 ...
2명 중 1번 째 사용자 조회 중 ...
1명 중 1번 째 사용자 조회 중 ...
3명 중 2번 째 사용자 조회 중 ...
2명 중 2번 째 사용자 조회 중 ...
> 총 1 명 사용자 비동기 조회 완료!
3명 중 3번 째 사용자 조회 중 ...
> 총 2 명 사용자 비동기 조회 완료!
> 총 3 명 사용자 비동기 조회 완료!
>>> 비동기 처리 총 소요 시간: 3.0031800270080566
p.s) ChatGPT 짱이다... 구글링하는 수고를 대폭 줄여 주고 설명도 더 잘 해준다 🥰
41.1 코루틴에 값 보내기 (간결한 그림과 쉬운 설명, 예시)
https://dojang.io/mod/page/view.php?id=2418
[파이썬] asyncio로 비동기 처리하기 (최고의 예제 코드 👍)
https://www.daleseo.com/python-asyncio/
071 비동기 방식으로 프로그래밍하려면? ― asyncio
https://wikidocs.net/125092
47.10 asyncio 사용하기 (asyncio deep dive)
https://dojang.io/mod/page/view.php?id=2469
코루틴과 태스크 (파이썬 공식 문서)
https://docs.python.org/ko/3/library/asyncio-task.html
사수님 두뇌 🧠
https://best_senior_you_are_god/
좋은 정보 감사합니다~!