비동기와 코루틴
1. 비동기 프로그래밍
비동기 처리 방식이 왜 필요한지 아래 예시를 들어 살펴보도록 하겠습니다. 먼저 동기 방식에 대해 살펴보겠습니다. 동기는 순차적으로 처리되는 방식입니다.
- (10시) licat : 로봇 청소기를 돌립니다.
- (11시) licat : 빨래를 합니다.
- (12시) licat : 설거지를 합니다.
- (01시) licat : 요리를 합니다.
위의 예시는 동기 방식으로 처리되는 방식입니다. 순차적으로 처리되기 때문에 로봇 청소기를 돌리고 나서 빨래를 하고, 빨래를 하고 나서 설거지를 하고, 설거지를 하고 나서 요리를 합니다.
반면 비동기 방식은 순차적으로 처리되지 않습니다. 아래 예시를 살펴보겠습니다.
- (10시) licat : 로봇 청소기를 돌리면서
- (10시) licat : 빨래를 합니다.
- (10시) licat : 설거지를 합니다.
- (10시) licat : 요리를 하려고 물도 끓입니다.
우리는 당연히 비동기 처리가 효율적이라는 것을 알고 있습니다. 다만 컴퓨터는 항상 모든 일을 순차적으로 진행하려 합니다. 앞에 일이 다 끝나야만 뒤에 일을 하는 것이죠. 이는 특히 I/O 작업, 네트워크 요청, 긴 계산 과정 등을 처리할 때 유용합니다.
카페에서 주문을 받는 아래 이미지를 예로 들어보겠습니다. 만약 주문을 받는 점원이 주문을 받고 모든 음료를 만들어야만 다음 주문을 받는다면 어떻게 될까요? 이런 경우가 없을 것 같지만 code에서는 이런 경우가 많이 발생합니다.
이번에는 실제 프로젝트에서 사용하는 예시를 살펴보도록 하겠습니다. 아래 이미지는 위니브월드라는 서비스입니다. 캐릭터를 움직이며 프로그래밍을 배울 수 있는 툴이죠. 여기서 왼쪽 소스코드와 오른쪽 캐릭터는 비동기로 움직이고, 오른쪽 소스코드는 동기로 움직입니다. 만약 이것이 동기로 움직인다면 어떤 일이 발생될까요? 부드러운 애니메이션은 적용시킬 수 없을겁니다. 코드의 실행 속도는 워낙 빠르니까요.
위니브월드 Beta1.1 동기 vs 비동기 프로그래밍 개념
- 동기 프로그래밍(Synchronous Programming): 코드가 순차적으로 실행되며, 한 작업이 완료될 때까지 다음 작업은 대기합니다. 이 방식은 코드의 흐름을 이해하기 쉽지만, 리소스 활용도가 낮을 수 있습니다.
- 비동기 프로그래밍(Asynchronous Programming): 동시에 여러 작업을 진행할 수 있으며, 한 작업이 완료될 때까지 기다리지 않고 다른 작업을 수행합니다. 이벤트 루프와 콜백 함수, 프로미스(Promise), async/await 구문 등을 활용하여 작업을 관리합니다.
코드가 순차적으로 실행되며, 특정 작업이 완료될 때까지 프로그램이 기다리는 방식입니다. 해당 실습은 로컬에서도 실습해보시길 권합니다. 진행하고 있는 환경인 colab에서 진행할 수 있도록 별도의 라이브러리를 설치하여 진행하겠습니다.
Google Colab의 환경에서는 이미 기본적으로 이벤트 루프가 실행 중입니다. 이 이벤트 루프는 Google Colab 환경의 비동기 작업을 처리하기 위해 사용됩니다. 그러므로, Google Colab에서는 asyncio.run()
함수를 직접 호출하면 "cannot be called from a running event loop"와 같은 에러 메시지가 출력됩니다. 이를 해결하려면 아래와 같은 코드를 추가해야 합니다.
!pip install nest_asyncio
!pip install nest_asyncio
import nest_asyncio
nest_asyncio.apply()
import nest_asyncio
nest_asyncio.apply()
import time
def job(number):
print(f"Job {number} started")
time.sleep(3) # 이 time.sleep이 매우 오래 걸리는 작업 이라 가정하고 그 효율을 생각해봅시다. 일반 sleep은 CPU를 쉬게 합니다.
print(f"Job {number} completed")
job(1)
job(2)
job(3)
import time
def job(number):
print(f"Job {number} started")
time.sleep(3) # 이 time.sleep이 매우 오래 걸리는 작업 이라 가정하고 그 효율을 생각해봅시다. 일반 sleep은 CPU를 쉬게 합니다.
print(f"Job {number} completed")
job(1)
job(2)
job(3)
이제 이를 해결하기 위한 비동기 프로그래밍을 해보도록 하겠습니다. 비동기 프로그래밍은 동시에 여러 작업을 진행할 수 있습니다. 이때, 이벤트 루프와 콜백 함수 등을 활용하여 작업을 관리합니다. 이 작업은 colab에서는 위에 nest_asyncio 모듈이 설치된 다음 진행하실 수 있습니다.
import asyncio
async def job(number):
print(f"Job {number} started")
await asyncio.sleep(1) # 매우 오래 걸리는 작업, asyncio.sleep은 비동기 처리를 할 수 있도록 합니다.(다른 작업이 가능합니다.)
print(f"Job {number} completed")
async def main():
await asyncio.gather(job(1), job(2), job(3)) # await asyncio.wait([job(1), job(2), job(3)])
asyncio.run(main())
print('hello world')
import asyncio
async def job(number):
print(f"Job {number} started")
await asyncio.sleep(1) # 매우 오래 걸리는 작업, asyncio.sleep은 비동기 처리를 할 수 있도록 합니다.(다른 작업이 가능합니다.)
print(f"Job {number} completed")
async def main():
await asyncio.gather(job(1), job(2), job(3)) # await asyncio.wait([job(1), job(2), job(3)])
asyncio.run(main())
print('hello world')
일부 작업은 비동기 프로그래밍을 중간에 동기로 바꿔야 하는 순간이 오기도 합니다. 아래와 같이 비동기 프로그래밍을 동기로 만들 수 있습니다.
import asyncio
async def job(number):
print(f"Job {number} started")
await asyncio.sleep(1) # 매우 오래 걸리는 작업
print(f"Job {number} completed")
asyncio.run(job(1))
asyncio.run(job(2))
asyncio.run(job(3))
import asyncio
async def job(number):
print(f"Job {number} started")
await asyncio.sleep(1) # 매우 오래 걸리는 작업
print(f"Job {number} completed")
asyncio.run(job(1))
asyncio.run(job(2))
asyncio.run(job(3))
위와 동일한 동작을 하는 colab에서만 가능한 코드를 보도록 하겠습니다. .py
파일에서는 await이 함수 밖에 사용되는 것을 허락하지 않습니다.
import asyncio
async def job(number):
print(f"Job {number} started")
await asyncio.sleep(1) # 매우 오래 걸리는 작업
print(f"Job {number} completed")
await job(1)
await job(2)
await job(3)
import asyncio
async def job(number):
print(f"Job {number} started")
await asyncio.sleep(1) # 매우 오래 걸리는 작업
print(f"Job {number} completed")
await job(1)
await job(2)
await job(3)
다음 코드는 .py
파일에서 위와 동일하게 작동하는 코드입니다.
import asyncio
async def job(number):
print(f"Job {number} started")
await asyncio.sleep(1) # 매우 오래 걸리는 작업
print(f"Job {number} completed")
async def main():
await job(1)
await job(2)
await job(3)
asyncio.run(main())
import asyncio
async def job(number):
print(f"Job {number} started")
await asyncio.sleep(1) # 매우 오래 걸리는 작업
print(f"Job {number} completed")
async def main():
await job(1)
await job(2)
await job(3)
asyncio.run(main())
1.2 코루틴(Coroutines)
코루틴은 비동기 프로그래밍의 핵심 개념 중 하나로, 파이썬에서는 async
/ await
구문을 통해 코루틴을 간편하게 구현할 수 있습니다. 코루틴은 파이썬 3.5부터 async
/ await
구문을 통해 지원되기 시작했습니다.
- 코루틴 함수:
async def
키워드를 사용하여 정의된 함수입니다. 이 함수는 코루틴 객체를 반환하며, 비동기적으로 실행될 수 있습니다. - 코루틴 객체: 코루틴 함수가 호출될 때 생성되는 객체로, 이벤트 루프에서 실행될 준비가 된 비동기 작업을 나타냅니다.
await
키워드: 코루틴 안에서 다른 코루틴을 호출하고 완료를 기다릴 때 사용됩니다.await
키워드를 만나면 해당 코루틴은 일시 중단되고, 호출된 코루틴의 작업이 완료될 때까지 다른 작업으로 제어가 넘어갑니다.
코루틴은 asyncio
모듈의 함수를 통해 실행됩니다.
asyncio.run(coro)
: 새 이벤트 루프를 만들고 주어진 코루틴coro
를 실행한 후, 이벤트 루프를 닫습니다.asyncio.gather(*coros)
: 여러 코루틴을 동시에 실행하도록 스케줄링합니다. 이 함수는 모든 코루틴이 완료될 때까지 기다린 후 결과를 반환합니다.
아래 함수는 일반 함수입니다.
def job():
print('job')
def job():
print('job')
아래 코드는 async를 붙인 함수, 코루틴 함수입니다. await 키워드를 만나면 코루틴 실행을 잠시 중단하고, 코루틴의 작업이 완료될 때까지 기다린 후 결과를 반환합니다.
async def job():
print('job')
print(job) # <function job at 0x7fc8cb38ef80>, 코루틴 객체 반환
job() # <coroutine object job at 0x7fc8cb32bc30>, print('job')이 실행되진 않습니다!
async def job():
print('job')
print(job) # <function job at 0x7fc8cb38ef80>, 코루틴 객체 반환
job() # <coroutine object job at 0x7fc8cb32bc30>, print('job')이 실행되진 않습니다!
async def main():
return await job() # 'job' 출력을 기다림
main() # <coroutine object main at 0x7fc8cb22bf40>
print(await main()) # None
async def main():
return await job() # 'job' 출력을 기다림
main() # <coroutine object main at 0x7fc8cb22bf40>
print(await main()) # None
코루틴은 비동기 프로그래밍에서 중요한 역할을 합니다. 여러 I/O 작업이나 네트워크 요청을 병렬로 처리할 때, 코루틴은 코드의 복잡성을 줄이고 성능을 향상시키는 데 큰 도움을 줍니다.
2. 이벤트 루프(Event Loop)
이벤트 루프는 비동기 프로그래밍에서 중심적인 역할을 하는 구성 요소입니다. 이벤트 루프의 주요 기능은 애플리케이션의 흐름을 제어하고, 비동기 작업을 관리하는 것입니다.
2.1 이벤트 루프의 작동 원리
- 작업 관리: 이벤트 루프는 실행할 코루틴과 작업을 관리합니다. 루프는 작업이 완료될 때까지 실행하고, 완료된 작업의 결과를 반환합니다.
- 이벤트 모니터링: 이벤트 루프는 I/O 이벤트(예: 네트워크 요청, 사용자 입력)를 모니터링합니다. 적절한 이벤트 핸들러 또는 콜백 함수가 이벤트에 응답합니다.
- 비동기 작업 스케줄링: 이벤트 루프는 비동기 작업을 스케줄링하고, 실행 준비가 된 작업을 실행합니다. 이를 통해 작업이 병렬로 실행되는 것처럼 보이게 합니다.
2.2 이벤트 루프 사용 예시
import asyncio
async def async_task():
# 비동기 작업 정의
...
# 이벤트 루프 생성 및 실행
loop = asyncio.get_event_loop()
try:
loop.run_until_complete(async_task()) # async_task 실행
finally:
loop.close() # 이벤트 루프 종료
import asyncio
async def async_task():
# 비동기 작업 정의
...
# 이벤트 루프 생성 및 실행
loop = asyncio.get_event_loop()
try:
loop.run_until_complete(async_task()) # async_task 실행
finally:
loop.close() # 이벤트 루프 종료
이 코드는 코루틴 async_task
를 실행하기 위해 이벤트 루프를 사용하는 기본적인 예시입니다.
2.3 이벤트 루프의 중요성
이벤트 루프를 사용하여 여러개의 이벤트 루프를 관리할 수 있습니다. 이러한 비동기 프로그래밍은 코드 전체의 성능 향상을 도모할 수 있습니다. 또한 응답성을 개선하여 사용자의 만족도를 올릴 수도 있습니다.
코루틴을 실행시킬 때 하나의 코루틴이 이벤트 루프가 되진 않습니다. 대신, 이벤트 루프 내에 여러 코루틴을 묶어 비동기 프로그래밍을 할 수 있습니다.