고성능 파이썬: Asyncio
Takashi Yamamoto
Infrastructure Engineer · Leapcell

동시성 프로그래밍은 여러 작업을 동시에 실행하는 프로그래밍 접근 방식입니다. Python에서 asyncio
는 비동기 프로그래밍을 구현하기 위한 강력한 도구입니다. 코루틴 개념을 기반으로 하는 asyncio
는 I/O 집약적인 작업을 효율적으로 처리할 수 있습니다. 이 기사에서는 asyncio
의 기본 원리와 사용법을 소개합니다.
asyncio가 필요한 이유
I/O 작업을 처리할 때 멀티스레딩을 사용하면 일반적인 단일 스레드에 비해 효율성을 크게 향상시킬 수 있다는 것을 알고 있습니다. 그렇다면 왜 여전히 asyncio
가 필요할까요?
멀티스레딩은 많은 장점이 있고 널리 사용되지만, 몇 가지 제한 사항도 있습니다.
- 예를 들어, 멀티스레딩의 실행 프로세스는 쉽게 중단될 수 있으므로 경쟁 조건이 발생할 수 있습니다.
- 또한 스레드 전환 자체에 일정한 비용이 발생하며 스레드 수를 무한정 늘릴 수 없습니다. 따라서 I/O 작업이 매우 많으면 멀티스레딩이 높은 효율성과 고품질 요구 사항을 충족하지 못할 가능성이 높습니다.
asyncio
는 바로 이러한 문제를 해결하기 위해 등장했습니다.
동기 VS 비동기
먼저 동기(Sync)와 비동기(Async)의 개념을 구별해 보겠습니다.
- 동기란 작업이 순차적으로 실행된다는 의미입니다. 다음 작업은 이전 작업이 완료된 후에만 실행할 수 있습니다.
- 비동기란 서로 다른 작업을 번갈아 가며 실행할 수 있다는 의미입니다. 작업 중 하나가 차단되면 프로그램은 기다리지 않고 실행 가능한 작업을 찾아 계속합니다.
asyncio 작동 방식
- 코루틴:
asyncio
는 코루틴을 사용하여 비동기 연산을 구현합니다. 코루틴은async
키워드로 정의된 특수한 함수입니다. 코루틴에서await
키워드를 사용하여 현재 코루틴의 실행을 일시 중지하고 비동기 연산이 완료될 때까지 기다릴 수 있습니다. - 이벤트 루프: 이벤트 루프는
asyncio
의 핵심 메커니즘 중 하나입니다. 코루틴을 스케줄링하고 실행하며 코루틴 간 전환을 처리하는 역할을 합니다. 이벤트 루프는 실행 가능한 작업을 지속적으로 폴링합니다. 작업이 준비되면(예: I/O 작업이 완료되거나 타이머가 만료된 경우) 이벤트 루프는 이를 실행 대기열에 넣고 다음 작업으로 계속 진행합니다. - 비동기 작업:
asyncio
에서 비동기 작업을 생성하여 코루틴을 실행합니다. 비동기 작업은 코루틴을 await 가능한 객체로 캡슐화하고 처리를 위해 이벤트 루프에 제출하는asyncio.create_task()
함수에 의해 생성됩니다. - 비동기 I/O 연산:
asyncio
는 일련의 비동기 I/O 연산(예: 네트워크 요청, 파일 읽기 및 쓰기 등)을 제공하며,await
키워드를 통해 코루틴 및 이벤트 루프와 원활하게 통합할 수 있습니다. 비동기 I/O 연산을 사용하면 I/O 완료를 기다리는 동안의 차단을 피할 수 있어 프로그램 성능과 동시성을 향상시킬 수 있습니다. - 콜백:
asyncio
는 콜백 함수를 사용하여 비동기 연산 결과를 처리하는 기능도 지원합니다.asyncio.ensure_future()
함수를 사용하여 콜백 함수를 await 가능한 객체로 캡슐화하고 처리를 위해 이벤트 루프에 제출할 수 있습니다. - 동시 실행:
asyncio
는 여러 코루틴 작업을 동시에 실행할 수 있습니다. 이벤트 루프는 작업 준비 상태에 따라 코루틴 실행을 자동으로 스케줄링하므로 효율적인 동시 프로그래밍이 가능합니다.
요약하면 asyncio
의 작동 원리는 코루틴 및 이벤트 루프 메커니즘을 기반으로 합니다. 비동기 연산에 코루틴을 사용하고 이벤트 루프가 코루틴 스케줄링 및 실행을 담당하도록 함으로써 asyncio
는 효율적인 비동기 프로그래밍 모델을 구현합니다.
코루틴 및 비동기 프로그래밍
코루틴은 asyncio
에서 중요한 개념입니다. 코루틴은 스레드 전환 오버헤드 없이 작업 간에 빠르게 전환할 수 있는 경량 실행 단위입니다. 코루틴은 async
키워드로 정의할 수 있으며, 특정 작업이 완료된 후 코루틴의 실행을 일시 중지했다가 재개하는 데 await
키워드를 사용합니다.
다음은 코루틴을 사용하여 비동기 프로그래밍을 수행하는 방법을 보여주는 간단한 샘플 코드입니다.
import asyncio async def hello(): print("Hello") await asyncio.sleep(1) # 시간 소모가 많은 연산 시뮬레이션 print("World") # 이벤트 루프 생성 loop = asyncio.get_event_loop() # 코루틴을 이벤트 루프에 추가하고 실행 loop.run_until_complete(hello())
이 예제에서 함수 hello()
는 async
키워드로 정의된 코루틴입니다. 코루틴 내부에서 await
를 사용하여 실행을 일시 중지할 수 있습니다. 여기서 asyncio.sleep(1)
은 시간 소모가 많은 연산을 시뮬레이션하는 데 사용됩니다. run_until_complete()
메서드는 코루틴을 이벤트 루프에 추가하고 실행합니다.
비동기 I/O 연산
asyncio
는 주로 네트워크 요청, 파일 읽기 및 쓰기와 같은 I/O 집약적인 작업을 처리하는 데 사용됩니다. 비동기 I/O 연산을 위한 일련의 API를 제공하며, await
키워드와 함께 사용하여 비동기 프로그래밍을 쉽게 구현할 수 있습니다.
다음은 asyncio
를 사용하여 비동기 네트워크 요청을 수행하는 방법을 보여주는 간단한 샘플 코드입니다.
import asyncio import aiohttp async def fetch(session, url): async with session.get(url) as response: return await response.text() async def main(): async with aiohttp.ClientSession() as session: html = await fetch(session, 'https://www.example.com') print(html) # 이벤트 루프 생성 loop = asyncio.get_event_loop() # 코루틴을 이벤트 루프에 추가하고 실행 loop.run_until_complete(main())
이 예제에서는 네트워크 요청에 aiohttp
라이브러리를 사용합니다. 함수 fetch()
는 코루틴입니다. session.get()
메서드를 통해 비동기 GET 요청을 시작하고 await
키워드를 사용하여 응답이 반환될 때까지 기다립니다. 함수 main()
은 또 다른 코루틴입니다. 내부에서 재사용을 위해 ClientSession
객체를 생성한 다음 fetch()
메서드를 호출하여 웹 페이지 콘텐츠를 가져와 출력합니다.
참고: 여기서는 requests
라이브러리 대신 aiohttp
를 사용합니다. requests
라이브러리는 asyncio
와 호환되지 않지만 aiohttp
라이브러리는 호환되기 때문입니다. 특히 강력한 기능을 발휘하려면 asyncio
를 잘 활용하기 위해 많은 경우에 해당 Python 라이브러리가 필요합니다.
여러 작업의 동시 실행
asyncio
는 asyncio.gather()
및 asyncio.wait()
와 같이 여러 작업을 동시에 실행하기 위한 몇 가지 메커니즘도 제공합니다. 다음은 이러한 메커니즘을 사용하여 여러 코루틴 작업을 동시에 실행하는 방법을 보여주는 샘플 코드입니다.
import asyncio async def task1(): print("Task 1 started") await asyncio.sleep(1) print("Task 1 finished") async def task2(): print("Task 2 started") await asyncio.sleep(2) print("Task 2 finished") async def main(): await asyncio.gather(task1(), task2()) # 이벤트 루프 생성 loop = asyncio.get_event_loop() # 코루틴을 이벤트 루프에 추가하고 실행 loop.run_until_complete(main())
이 예제에서는 두 개의 코루틴 작업 task1()
및 task2()
를 정의합니다. 둘 다 시간이 오래 걸리는 작업을 수행합니다. 코루틴 main()
은 asyncio.gather()
를 통해 이러한 두 작업을 동시에 시작하고 완료될 때까지 기다립니다. 동시 실행은 프로그램 실행 효율성을 향상시킬 수 있습니다.
어떻게 선택해야 할까요?
실제 프로젝트에서 멀티스레딩과 asyncio
중 무엇을 선택해야 할까요? 한 유명 인사가 이를 생생하게 요약했습니다.
if io_bound: if io_slow: print('Use Asyncio') else: print('Use multi-threading') elif cpu_bound: print('Use multi-processing')
- I/O 바운드이고 I/O 작업이 느려 많은 작업/스레드의 협업이 필요한 경우
asyncio
를 사용하는 것이 더 적절합니다. - I/O 바운드이지만 I/O 작업이 빠르고 제한된 수의 작업/스레드만 필요한 경우 멀티스레딩으로 충분합니다.
- CPU 바운드인 경우 프로그램 실행 효율성을 높이기 위해 멀티프로세싱이 필요합니다.
실습
목록을 입력합니다. 목록의 각 요소에 대해 0부터 이 요소까지의 모든 정수의 제곱 합계를 계산하려고 합니다.
동기 구현
import time def cpu_bound(number): return sum(i * i for i in range(number)) def calculate_sums(numbers): for number in numbers: cpu_bound(number) def main(): start_time = time.perf_counter() numbers = [10000000 + x for x in range(20)] calculate_sums(numbers) end_time = time.perf_counter() print('Calculation takes {} seconds'.format(end_time - start_time)) if __name__ == '__main__': main()
실행 시간은 Calculation takes 16.00943413000002 seconds
입니다.
concurrent.futures를 사용한 비동기 구현
import time from concurrent.futures import ThreadPoolExecutor, ProcessPoolExecutor, as_completed def cpu_bound(number): return sum(i * i for i in range(number)) def calculate_sums(numbers): with ProcessPoolExecutor() as executor: results = executor.map(cpu_bound, numbers) results = [result for result in results] print(results) def main(): start_time = time.perf_counter() numbers = [10000000 + x for x in range(20)] calculate_sums(numbers) end_time = time.perf_counter() print('Calculation takes {} seconds'.format(end_time - start_time)) if __name__ == '__main__': main()
실행 시간은 Calculation takes 7.314132894999999 seconds
입니다.
이 개선된 코드에서는 concurrent.futures.ProcessPoolExecutor
를 사용하여 프로세스 풀을 만들고 executor.map()
메서드를 사용하여 작업을 제출하고 결과를 얻습니다. executor.map()
을 사용한 후 결과를 얻어야 하는 경우 결과를 목록으로 반복하거나 다른 방법을 사용하여 결과를 처리할 수 있습니다.
멀티프로세싱 구현
import time import multiprocessing def cpu_bound(number): return sum(i * i for i in range(number)) def calculate_sums(numbers): with multiprocessing.Pool() as pool: pool.map(cpu_bound, numbers) def main(): start_time = time.perf_counter() numbers = [10000000 + x for x in range(20)] calculate_sums(numbers) end_time = time.perf_counter() print('Calculation takes {} seconds'.format(end_time - start_time)) if __name__ == '__main__': main()
실행 시간은 Calculation takes 5.024221667 seconds
입니다.
concurrent.futures.ProcessPoolExecutor
와 multiprocessing
은 모두 Python에서 멀티프로세스 동시성을 구현하기 위한 라이브러리입니다. 몇 가지 차이점이 있습니다.
- 인터페이스 기반 캡슐화:
concurrent.futures.ProcessPoolExecutor
는concurrent.futures
모듈에서 제공하는 고급 인터페이스입니다. 기본 멀티프로세스 함수를 캡슐화하여 멀티프로세스 코드를 더 쉽게 작성할 수 있습니다. 반면multiprocessing
은 Python의 표준 라이브러리 중 하나이며, 완전한 멀티프로세스 지원을 제공하고 프로세스를 직접 조작할 수 있습니다. - API 사용법:
concurrent.futures.ProcessPoolExecutor
의 사용법은 스레드 풀의 사용법과 유사합니다. 실행을 위해 프로세스 풀에 호출 가능한 객체(예: 함수)를 제출하고 실행 결과를 얻는 데 사용할 수 있는Future
객체를 반환합니다.multiprocessing
은 더 낮은 수준의 프로세스 관리 및 통신 인터페이스를 제공합니다. 프로세스를 명시적으로 만들고 시작 및 제어할 수 있으며, 큐 또는 파이프를 사용하여 여러 프로세스 간의 통신을 수행할 수 있습니다. - 확장성 및 유연성:
multiprocessing
은 더 낮은 수준의 인터페이스를 제공하므로concurrent.futures.ProcessPoolExecutor
에 비해 더 유연합니다. 프로세스를 직접 조작하여 각 프로세스에 대한 보다 세분화된 제어를 달성할 수 있습니다(예: 프로세스 우선 순위를 설정하고 프로세스 간에 데이터를 공유).concurrent.futures.ProcessPoolExecutor
는 간단한 작업 병렬화에 더 적합하며, 많은 기본 세부 정보를 숨겨 멀티프로세스 코드를 더 쉽게 작성할 수 있습니다. - 플랫폼 간 지원:
concurrent.futures.ProcessPoolExecutor
와multiprocessing
은 모두 플랫폼 간 멀티프로세스 지원을 제공하며 다양한 운영 체제에서 사용할 수 있습니다.
요약하면 concurrent.futures.ProcessPoolExecutor
는 기본 멀티프로세스 함수를 캡슐화하는 고급 인터페이스로, 간단한 멀티프로세스 작업 병렬화에 적합합니다. multiprocessing
은 더 낮은 수준의 라이브러리로, 더 많은 제어 및 유연성을 제공하며 프로세스에 대한 세분화된 제어가 필요한 시나리오에 적합합니다. 특정 요구 사항에 따라 적절한 라이브러리를 선택해야 합니다. 간단한 작업 병렬화일 경우 concurrent.futures.ProcessPoolExecutor
를 사용하여 코드를 단순화할 수 있습니다. 더 낮은 수준의 제어 및 통신이 필요한 경우 multiprocessing
라이브러리를 사용할 수 있습니다.
요약
멀티스레딩과 달리 asyncio
는 단일 스레드이지만 내부 이벤트 루프 메커니즘을 통해 여러 가지 다른 작업을 동시에 실행하고 멀티스레딩보다 더 큰 자율 제어를 수행할 수 있습니다.
asyncio
의 작업은 작동 중에 중단되지 않으므로 경쟁 조건이 발생하지 않습니다.
특히 I/O 작업이 많은 시나리오에서 asyncio
는 멀티스레딩보다 더 높은 작동 효율성을 제공합니다. asyncio
의 작업 전환 비용이 스레드 전환 비용보다 훨씬 적고 asyncio
가 시작할 수 있는 작업 수가 멀티스레딩의 스레드 수보다 훨씬 많기 때문입니다.
그러나 많은 경우에 asyncio
를 사용하려면 이전 예의 aiohttp
와 같은 특정 타사 라이브러리의 지원이 필요하다는 점에 유의해야 합니다. 그리고 I/O 작업이 빠르고 많지 않으면 멀티스레딩을 사용하여 문제를 효과적으로 해결할 수도 있습니다.
asyncio
는 비동기 프로그래밍을 구현하기 위한 Python 라이브러리입니다.- 코루틴은
asyncio
의 핵심 개념으로,async
및await
키워드를 통해 비동기 연산을 구현합니다. asyncio
는 비동기 I/O 연산을 위한 강력한 API를 제공하며 I/O 집약적인 작업을 쉽게 처리할 수 있습니다.asyncio.gather()
와 같은 메커니즘을 통해 여러 코루틴 작업을 동시에 실행할 수 있습니다.
Leapcell: FastAPI, Flask 및 기타 Python 애플리케이션을 위한 이상적인 플랫폼
마지막으로 Flask/FastAPI 배포를 위한 이상적인 플랫폼인 Leapcell을 소개합니다.
Leapcell은 최신 분산 애플리케이션을 위해 특별히 설계된 클라우드 컴퓨팅 플랫폼입니다. 사용한 만큼 지불하는 가격 모델은 유휴 비용이 발생하지 않도록 보장하므로 사용자는 실제로 사용하는 리소스에 대해서만 비용을 지불합니다.
- 다국어 지원
- JavaScript, Python, Go 또는 Rust로 개발을 지원합니다.
- 무제한 프로젝트 무료 배포
- 사용량에 따라 요금이 부과됩니다. 요청이 없으면 요금이 부과되지 않습니다.
- 비교할 수 없는 비용 효율성
- 사용한 만큼 지불하며 유휴 요금이 없습니다.
- 예를 들어 $25는 694만 건의 요청을 지원할 수 있으며 평균 응답 시간은 60밀리초입니다.
- 단순화된 개발자 경험
- 쉬운 설정을 위한 직관적인 사용자 인터페이스입니다.
- 완전 자동화된 CI/CD 파이프라인 및 GitOps 통합입니다.
- 실행 가능한 통찰력을 제공하는 실시간 메트릭 및 로그입니다.
- 간편한 확장성 및 고성능
- 고도의 동시성을 쉽게 처리할 수 있도록 자동 확장됩니다.
- 운영 오버헤드가 없어 개발자가 개발에 집중할 수 있습니다.
자세한 내용은 설명서를 참조하십시오! Leapcell 트위터: https://x.com/LeapcellHQ