MINERVA/Python
2024. 10. 19. 16:15
반응형
'십시일반'이라는 말이 있듯이, 프로그램 성능을 향상시키기 위해서는 모든 개발 언어에서, multi-threading과 async는 매우 중요합니다. 저 역시, 개발자로서 초기에 매우 많은 시간을 투자하였습니다.
근래에는, 파이썬으로 많은 작업을 하면서 개발을 하는데 무리는 없지만, 간단하게 해당 내용을 정리 및 기록하고자 합니다.
[코드]
- 모든 작업은 동일한 print_numbers()와 print_letters() 함수를 각각 쓰레딩 및 async 방법으로 실행시 성능 테스트
- 0) single_thread 실행을 기준으로 추가적인 1)threading ~5)gevent방법을 비교해보자.
import os
import threading
import concurrent.futures
import multiprocessing
import asyncio
import gevent
import time
##################
# 공통 함수
###
def print_numbers():
for i in range(1, 6):
time.sleep(1)
def print_letters():
for letter in ['A', 'B', 'C', 'D', 'E']:
time.sleep(1)
##################
# 0) single_thread 에서 순차적으로 실행
###
def single_thread_module():
start_time = time.time()
print_numbers()
print_letters()
end_time = time.time()
print(f"Single-thread execution time: {end_time - start_time:.2f} seconds")
##################
# 1) 표준 라이브러리의 threading 모듈을 사용
# - 글로벌 인터프리터 락(GIL)로 인해 CPU 바운드 작업에는 한계가 있음.
###
def threading_module():
start_time = time.time()
# Thread 객체 생성
t1 = threading.Thread(target=print_numbers)
t2 = threading.Thread(target=print_letters)
# Thread 시작
t1.start()
t2.start()
# Thread 료될 때까지 대기
t1.join()
t2.join()
end_time = time.time()
print(f"Threading execution time: {end_time - start_time:.2f} seconds")
##################
# 2) concurrent.futures.ThreadPoolExecutor 사용
# [비교]
# 쓰레드를 직접 관리하는 대신, 작업을 스레드 풀에 제출하고, 비동기적으로 처리
# 자동으로 쓰레드 종료를 관리
###
def concurrent_mudule():
start_time = time.time()
with concurrent.futures.ThreadPoolExecutor() as executor:
future1 = executor.submit(print_numbers)
future2 = executor.submit(print_letters)
concurrent.futures.wait([future1, future2])
end_time = time.time()
print(f"ThreadPoolExecutor execution time: {end_time - start_time:.2f} seconds")
##################
# 3) multiprocessing 사용
# - 파이썬 GIL(Global Interpreter Lock) 특성 때문에 CPU 바운드 작업경우 멀티 쓰레딩 성능 하락이 발생에 대한 해결책!
# 그 이유? 멀티 프로세싱을 하기때문에 간섭(?이 발생하지 않음
###
def multiprocessing_module():
start_time = time.time()
p1 = multiprocessing.Process(target=print_numbers)
p2 = multiprocessing.Process(target=print_letters)
p1.start()
p2.start()
p1.join()
p2.join()
end_time = time.time()
print(f"Multiprocessing execution time: {end_time - start_time:.2f} seconds")
##################
# 4) asyncio 모듈을 사용한 비동기 프로그래맹(멀티 쓰레딩 하님)
# - asyncio는 코루틴을 사용해 비동기적으로 I/O 바운드 작업을 수행(CPU 바운드 작업 비추)
# - 단일 스레드에서 실행되지만, 비동기 방식으로 여러 작업을 동시에 처리(동시성 지원)
###
async def async_print_numbers():
for i in range(1, 6):
await asyncio.sleep(1)
async def async_print_letters():
for letter in ['A', 'B', 'C', 'D', 'E']:
await asyncio.sleep(1)
async def main():
task1 = asyncio.create_task(async_print_numbers())
task2 = asyncio.create_task(async_print_letters())
await task1
await task2
def asyncio_module():
start_time = time.time()
asyncio.run(main())
end_time = time.time()
print(f"Asyncio execution time: {end_time - start_time:.2f} seconds")
##################
# 5) gevent
# - gevent는 코루틴을 기반으로 한 라이브러리로, 네트워크 작업 등에서 동시성을 극대화할
# - 비동기 방식이지만, 코드가 매우 간결해지는 장점
###
def gevent_module():
start_time = time.time()
g1 = gevent.spawn(print_numbers)
g2 = gevent.spawn(print_letters)
gevent.joinall([g1, g2])
end_time = time.time()
print(f"Gevent execution time: {end_time - start_time:.2f} seconds")
if __name__ == '__main__':
print(f'{__file__}')
print(f'{os.path.dirname(__file__)}')
single_thread_module()
threading_module()
concurrent_mudule()
multiprocessing_module()
asyncio_module()
gevent_module()
[실행 결과]
D:\mmPRJ\64B\mmFNAVI\mmThread_Work.py
Single-thread execution time: 10.07 seconds
Threading execution time: 5.03 seconds
ThreadPoolExecutor execution time: 5.04 seconds
Multiprocessing execution time: 5.31 seconds
Asyncio execution time: 5.02 seconds
Gevent execution time: 10.06 seconds
[정리]
- 파이썬에서 멀티쓰레딩을 구현하는 방법은 여러 가지가 있으며, 사용하려는 작업의 성격(예: CPU 바운드, I/O 바운드)에 따라 적합한 방법을 선택해야 합니다.
- GIL을 피하고 CPU 바운드 작업인 경우는 multiprocessing 사용
- asyncio는 단일 스레드에서 비동기 처리를 지원하는 I/O 바운드 작업에 적합
- 테스트 방법이 CPU 바운드 작업이 아닌 단순 지연기 때문에, 멀티쓰레딩/비동기 방식이 비슷한 성능을 보일 것으로 예상되지만, 상황에 따라 다를 수 있습니다.
반응형