파이썬 Asyncio 코루틴, 이벤트 루프 및 Async/Await 심층 분석: 기본 원리 파헤치기
Emily Parker
Product Engineer · Leapcell

소개
오늘날의 상호 연결된 세계에서 애플리케이션은 네트워크 요청, 데이터베이스 쿼리 또는 파일 I/O와 같이 외부 리소스를 기다리는 여러 작업을 수행해야 하는 시나리오를 자주 접합니다. 전통적으로 이러한 작업을 차단하면 리소스 활용이 비효율적이고 사용자 환경이 좋지 않게 됩니다. 비동기 프로그래밍의 패러다임 전환은 프로그램이 작업을 시작하고 첫 번째 작업이 완료되기를 기다리는 동안 다른 작업으로 전환할 수 있도록 합니다. 파이썬의 asyncio
라이브러리는 단일 스레드를 사용하여 동시 코드를 작성하기 위한 강력하고 우아한 프레임워크를 제공하여 개발자가 확장성이 뛰어나고 응답성이 뛰어난 애플리케이션을 구축할 수 있도록 지원합니다. 이 기사에서는 asyncio
의 기본 요소인 코루틴, 이벤트 루프 및 async/await
구문을 심층적으로 살펴보고 내부 작동 방식을 설명하며 협력적 멀티태스킹을 조율하는 방법을 보여줄 것입니다.
핵심 개념 설명
asyncio
의 메커니즘을 탐구하기 전에 기본 구성 요소에 대한 명확한 이해를 확립해 보겠습니다.
- 코루틴: 코루틴은 일시 중지하고 다시 시작할 수 있는 특수 함수입니다. 호출되면 완료될 때까지 실행되는 일반 함수와 달리 코루틴은 제어권을 호출자에게
yield
하여 다른 작업이 실행되도록 허용한 다음 중단된 지점에서 정확히 다시 시작할 수 있습니다. 파이썬에서 코루틴은async def
를 사용하여 정의됩니다. - 이벤트 루프: 이벤트 루프는
asyncio
의 중앙 오케스트레이터입니다. 이벤트(예: I/O 준비, 타이머, 완료된 작업)를 지속적으로 모니터링하고 적절한 코루틴에 디스패치합니다. 단일 스레드 스케줄러 역할을 하여 모든 비동기 작업의 실행 흐름을 관리합니다. - 작업: 작업은 코루틴에 대한 추상화로, 코루틴을 future와 유사한 객체로 래핑합니다. 코루틴이 이벤트 루프에서 실행되도록 예약되면 작업이 됩니다. 작업은 이벤트 루프가 취소 및 완료를 포함하여 코루틴의 수명 주기를 관리할 수 있도록 합니다.
- Future:
Future
객체는 비동기 작업의 최종 결과를 나타냅니다. 결과를 얻거나 예외를 얻기 위해 'await'할 수 있는 저수준 객체입니다. 작업은 future를 기반으로 구축된 더 높은 수준의 추상화입니다. async
및await
: 이 키워드는 코루틴 작성을 더 자연스럽게 하고 비동기 작업과 상호 작용할 수 있도록 하는 구문 설탕입니다.async
는 함수를 코루틴으로 정의하여 await 가능하게 만듭니다.await
는 현재 코루틴의 실행을 일시 중지하고 await되는 'awaitable'(다른 코루틴,Task
또는Future
)이 완료될 때까지 기다리며 이벤트 루프가 다른 작업으로 전환할 수 있도록 합니다.
Asyncio의 내부 작동 방식
asyncio
의 힘은 이벤트 루프가 조율하는 협력적 스케줄링에서 비롯됩니다. 이러한 구성 요소가 함께 작동하는 방식을 분석해 보겠습니다.
코루틴이 작업을 기다리는 방식
네트워크에서 데이터를 가져오는 일반적인 동기 함수를 생각해 보겠습니다.
import time def fetch_data_sync(url): print(f"Fetching data synchronously from {url}...") time.sleep(2) # Simulate network latency print(f"Finished fetching data from {url}.") return {"data": f"content from {url}"} # Synchronous execution start_time = time.time() data1 = fetch_data_sync("http://example.com/api/1") data2 = fetch_data_sync("http://example.com/api/2") end_time = time.time() print(f"Synchronous execution time: {end_time - start_time:.2f} seconds")
이 동기 예제에서는 fetch_data_sync("http://example.com/api/2")
가 fetch_data_sync("http://example.com/api/1")
가 시뮬레이션된 2초 지연을 포함하여 완전히 완료된 후에만 시작됩니다.
이제 async def
및 await
를 사용하여 asyncio
로 이 변환을 어떻게 수행하는지 살펴보겠습니다.
import asyncio import time async def fetch_data_async(url): print(f"Fetching data asynchronously from {url}...") await asyncio.sleep(2) # Non-blocking sleep, yields control print(f"Finished fetching data from {url}.") return {"data": f"content from {url}"} async def main(): start_time = time.time() # Schedule both coroutines to run concurrently task1 = asyncio.create_task(fetch_data_async("http://example.com/api/1")) task2 = asyncio.create_task(fetch_data_async("http://example.com/api/2")) # Await their completion data1 = await task1 data2 = await task2 end_time = time.time() print(f"Asynchronous execution time: {end_time - start_time:.2f} seconds") print(f"Data 1: {data1}") print(f"Data 2: {data2}") if __name__ == "__main__": asyncio.run(main())
비동기 버전에서는 다음과 같습니다.
async def fetch_data_async(url):
는fetch_data_async
를 코루틴으로 선언합니다.await asyncio.sleep(2)
는 중요한 부분입니다. 실행이 이 줄에 도달하면 2초 동안 차단하는 대신fetch_data_async
코루틴이 일시 중지되고 제어권을 이벤트 루프로 yield합니다.- 이벤트 루프는 실행 준비가 된 다른 작업을 찾습니다.
main
함수에서는 두 개의 작업인task1
및task2
를 생성했습니다. task1
이 await된 후 이벤트 루프는task2
로 전환할 수 있으며, 이는 실행을 시작하고 결국asyncio.sleep(2)
를 await합니다.- 두 코루틴이 "잠자는 중"(
awaiting
)인 동안 이벤트 루프는 타이머를 모니터링합니다. 2초 후task1
에 대한asyncio.sleep(2)
가 완료되었음을 나타내는 신호를 받습니다. 그런 다음 중지된 지점에서task1
을 재개하여 "Finished fetching data..."를 인쇄하고 결과를 반환할 수 있습니다. - 마찬가지로
task2
도 잠금 완료 후 재개됩니다. - 마지막으로
main
의await task1
및await task2
는 각각의 결과를 검색합니다.
주요 내용은 await
가 전체 프로그램을 차단하는 것이 아니라 현재 코루틴만 차단하여 이벤트 루프가 다른 작업을 동시에 처리할 수 있도록 한다는 것입니다. 이것이 협력적 멀티태스킹입니다. 코루틴은 명시적으로 제어권을 넘겨줍니다.
이벤트 루프의 역할
이벤트 루프는 여러 I/O 작업을 동시에 효율적으로 모니터링하기 위해 저수준 구성 요소(종종 selectors
또는 Linux의 epoll
, macOS의 kqueue
또는 Windows의 IOCP
와 같은 플랫폼별 메커니즘)를 사용하여 구현됩니다. 차단 없이.
asyncio.run(main())
을 호출하면 일반적으로 다음이 발생합니다.
- 이벤트 루프 인스턴스가 생성됩니다(현재 스레드에 이미 실행 중인 인스턴스가 없으면).
main()
코루틴이 이 이벤트 루프에 예약됩니다.- 이벤트 루프가 주요 실행 주기를 시작합니다.
- 실행 준비가 된 작업(즉, 현재 await하지 않은 작업)을 선택합니다.
await
표현식에 도달할 때까지 해당 작업을 실행합니다.await
가 발생하면 현재 작업이 일시 중지되고 상태가 저장됩니다.- 이벤트 루프는 완료된 I/O 작업, 만료된 타이머 또는 기타 큐된 이벤트를 확인합니다.
asyncio.sleep
또는 네트워크 읽기와 같은 await된 작업이 완료되면 해당 일시 중지된 작업이 다시 시작되도록 표시됩니다.- 이벤트 루프는 다른 준비된 작업을 선택하고 사이클을 계속합니다.
- 모든 예약된 작업이 완료될 때까지 이 사이클이 계속됩니다.
aiohttp
를 사용한 실제 적용
aiohttp
와 같은 asyncio
호환 HTTP 클라이언트 라이브러리를 사용하여 여러 HTTP 요청을 동시에 만드는 일반적인 사용 사례를 보여 드리겠습니다.
import asyncio import aiohttp import time async def fetch_url(session, url): async with session.get(url) as response: return await response.text() async def main_http(): urls = [ "https://www.example.com", "https://www.google.com", "https://www.bing.com", "https://www.python.org" ] start_time = time.time() async with aiohttp.ClientSession() as session: tasks = [] for url in urls: task = asyncio.create_task(fetch_url(session, url)) tasks.append(task) # Gather all results concurrently responses = await asyncio.gather(*tasks) end_time = time.time() print(f"Fetched {len(urls)} URLs in {end_time - start_time:.2f} seconds") # print first 100 chars of each response for i, res in enumerate(responses): print(f"URL {urls[i]} content snippet: {res[:100]}...") if __name__ == "__main__": asyncio.run(main_http())
이 예제에서는 다음과 같습니다.
aiohttp.ClientSession()
은 비동기 컨텍스트 관리자로, 적절한 리소스 관리를 보장합니다.- 각
url
에 대해fetch_url
이 코루틴으로 호출됩니다.await session.get(url)
및await response.text()
호출은 비 차단입니다.session.get
이 네트워크 요청을 시작하면 제어권을 yield하여 이벤트 루프가 다음 요청을 시작할 수 있도록 합니다. asyncio.gather(*tasks)
는 여러 awaitable(우리의task
)을 가져와 동시에 실행하는 강력한 유틸리티입니다. 모든 작업이 완료될 때까지 기다리고 작업이 전달된 순서대로 결과를 반환합니다.
이것은 asyncio
를 사용하여 I/O 작업을 중첩하여 순차적으로 각 URL을 가져오는 것보다 총 실행 시간을 상당히 줄일 수 있음을 보여줍니다.
결론
코루틴, 이벤트 루프 및 async/await
구문의 핵심 개념을 갖춘 asyncio
는 효율적인 동시 파이썬 애플리케이션을 작성하는 강력하고 직관적인 방법을 제공합니다. 코루틴이 협력적으로 제어권을 yield하는 방식과 이벤트 루프가 실행을 조율하는 방식을 이해함으로써 개발자는 응답성이 뛰어나고 확장 가능한 시스템을 구축하기 위해 단일 스레드 비동기 프로그래밍의 잠재력을 최대한 활용할 수 있습니다. asyncio
는 단순한 라이브러리가 아닙니다. 파이썬에서 동시성에 대한 더 효율적이고 현대적인 접근 방식으로의 근본적인 변화입니다.