고급 Python 동시성: 멀티스레딩과 AsyncIO
Min-jun Kim
Dev Intern · Leapcell

Python 동시성 프로그래밍 탐구
Python 프로그래밍에서 멀티스레딩은 일반적으로 사용되는 동시성 프로그래밍 수단으로, 프로그램의 실행 효율성을 효과적으로 향상시킬 수 있으며, 특히 I/O 집약적인 작업을 처리할 때 유용합니다. Python은 threading
모듈의 도움으로 멀티스레딩 프로그래밍을 비교적 쉽게 만듭니다. 이 글에서는 threading
모듈의 기본 지식을 자세히 살펴보고 예제를 통해 멀티스레딩의 적용을 보여줍니다.
1. 멀티스레딩의 기본 개념
시작하기 전에 멀티스레딩 프로그래밍의 몇 가지 기본 개념을 먼저 이해해 보겠습니다.
- 스레드: 운영 체제가 작업 스케줄링을 수행하는 가장 작은 단위이며, 일반적으로 프로세스 내부에 존재합니다.
- 멀티스레딩: 동일한 프로그램에서 여러 스레드를 동시에 실행하는 것을 의미합니다.
- GIL (Global Interpreter Lock): Python 인터프리터의 글로벌 인터프리터 잠금으로, 한 번에 하나의 스레드만 Python 바이트코드를 실행할 수 있도록 제한합니다. 따라서 CPU 집약적인 작업에서는 멀티스레딩이 멀티 코어 프로세서를 최대한 활용할 수 없습니다.
2. threading 모듈의 기초
threading
모듈은 스레드를 생성하고 관리하기 위한 도구를 제공합니다. 다음은 threading
모듈에서 일반적으로 사용되는 클래스 및 함수입니다.
- Thread 클래스: 스레드를 생성하는 데 사용되는 클래스입니다.
Thread
클래스를 상속하고run
메서드를 구현하여 스레드의 실행 로직을 정의합니다. - start() 메서드: 스레드를 시작합니다.
- join() 메서드: 스레드가 실행을 마칠 때까지 기다립니다.
- active_count() 함수: 현재 활성 스레드 수를 가져옵니다.
3. 코드 연습: 멀티스레드 이미지 다운로드
다음은 예제를 통해 멀티스레딩의 적용을 보여줍니다. 멀티스레딩을 사용하여 일련의 이미지를 다운로드합니다.
import threading import requests from queue import Queue class LeapCellImageDownloader: def __init__(self, urls): self.urls = urls self.queue = Queue() def download_image(self, url): response = requests.get(url) if response.status_code == 200: filename = url.split("/")[-1] with open(filename, "wb") as f: f.write(response.content) print(f"Downloaded: {filename}") def worker(self): while True: url = self.queue.get() if url is None: break self.download_image(url) self.queue.task_done() def start_threads(self, num_threads=5): threads = [] for _ in range(num_threads): thread = threading.Thread(target=self.worker) thread.start() threads.append(thread) for url in self.urls: self.queue.put(url) self.queue.join() for _ in range(num_threads): self.queue.put(None) for thread in threads: thread.join() if __name__ == "__main__": image_urls = ["url1", "url2", "url3"] # 실제 이미지 URL로 대체 downloader = LeapCellImageDownloader(image_urls) downloader.start_threads()
이 예제에서는 LeapCellImageDownloader
클래스를 만들었습니다. 이 클래스에는 이미지를 다운로드하기 위한 worker
메서드가 포함되어 있습니다. 멀티스레딩을 통해 여러 이미지를 병렬로 다운로드하여 다운로드 효율성을 향상시킬 수 있습니다.
4. 코드 분석
- download_image 메서드: 이미지 다운로드의 특정 구현을 담당합니다.
- worker 메서드: 스레드의 실행 로직으로, 큐에서 다운로드 할 이미지 URL을 지속적으로 가져 와서
download_image
메서드를 호출합니다. - start_threads 메서드: 지정된 수의 스레드를 시작하고 이미지 URL을 큐에 넣고 모든 스레드가 실행을 마칠 때까지 기다립니다.
6. 스레드 안전성 및 잠금 메커니즘
멀티스레딩 프로그래밍에서는 여러 스레드가 공유 자원에 동시에 접근하기 때문에 경쟁 조건이 발생할 수 있습니다. 이 상황을 피하기 위해 잠금 메커니즘을 사용하여 특정 순간에 하나의 스레드만 공유 자원에 접근할 수 있도록 보장할 수 있습니다.
threading
모듈은 Lock
클래스를 제공합니다. 이를 통해 잠금을 생성할 수 있습니다. acquire
메서드를 사용하여 잠금을 획득하고 release
메서드를 사용하여 잠금을 해제합니다. 다음은 간단한 예입니다.
import threading leapcell_counter = 0 leapcell_counter_lock = threading.Lock() def increment_counter(): global leapcell_counter for _ in range(1000000): with leapcell_counter_lock: leapcell_counter += 1 def main(): thread1 = threading.Thread(target=increment_counter) thread2 = threading.Thread(target=increment_counter) thread1.start() thread2.start() thread1.join() thread2.join() print("LeapCell Counter:", leapcell_counter) if __name__ == "__main__": main()
이 예제에서는 전역 변수 leapcell_counter
를 만들고 잠금을 사용하여 두 스레드가 leapcell_counter
를 동시에 수정할 때 경쟁 조건이 발생하지 않도록 보장했습니다.
7. 멀티스레딩의 적용 가능한 시나리오
멀티스레딩은 네트워크 요청, 파일 읽기 및 쓰기 등과 같은 I/O 집약적인 작업을 처리하는 데 적합합니다. 이러한 시나리오에서 스레드는 I/O를 기다리는 동안 CPU를 양보하여 다른 스레드가 실행할 기회를 제공하고 프로그램의 전체 효율성을 향상시킬 수 있습니다.
그러나 CPU 집약적인 작업을 처리 할 때는 Python의 GIL로 인해 멀티스레딩이 멀티 코어 프로세서를 최대한 활용할 수 없어 성능 병목 현상이 발생할 수 있습니다. CPU 집약적인 작업의 경우 멀티프로세싱 프로그래밍 또는 다른 동시 모델을 사용하는 것이 좋습니다.
9. 예외 처리 및 멀티스레딩
멀티스레딩 프로그래밍에서 예외 처리는 더 복잡해질 수 있습니다. 각 스레드에는 자체 실행 컨텍스트가 있으므로 한 스레드에서 예외가 발생했지만 다른 스레드에서 잡힐 수 있습니다. 예외를 효과적으로 처리하려면 각 스레드에서 적절한 예외 처리 메커니즘을 사용해야합니다.
import threading def leapcell_thread_function(): try: # 예외를 발생시킬 수 있는 일부 작업 result = 10 / 0 except ZeroDivisionError as e: print(f"Exception in LeapCell thread: {e}") if __name__ == "__main__": thread = threading.Thread(target=leapcell_thread_function) thread.start() thread.join() print("Main thread continues...")
이 예제에서 스레드의 leapcell_thread_function
의 나누기 연산은 ZeroDivisionError
예외를 발생시킬 수 있습니다. 이 예외를 잡고 처리하기 위해 스레드의 코드 블록에서 try-except
문을 사용했습니다.
10. 멀티스레딩에 대한 주의 사항
멀티스레딩 프로그래밍을 할 때는 특별한 주의가 필요한 몇 가지 일반적인 주의 사항이 있습니다.
- 스레드 안전성: 여러 스레드가 공유 자원에 동시에 접근할 때 데이터 경쟁 및 불일치가 발생하지 않도록 보장합니다.
- 교착 상태: 여러 스레드가 서로 잠금을 해제하기를 기다릴 때 교착 상태가 발생할 수 있으며 신중한 설계 및 잠금 사용이 필요합니다.
- GIL 제한: Python의 글로벌 인터프리터 잠금은 CPU 집약적인 작업에서 멀티스레딩의 성능 향상을 제한할 수 있습니다.
- 예외 처리: 한 스레드에서 예외가 발생했지만 다른 스레드에서 발생하지 못하도록 각 스레드에서 예외를 적절히 처리해야합니다.
11. 멀티스레딩의 성능 최적화
경우에 따라 일부 기술을 통해 멀티스레드 프로그램의 성능을 최적화할 수 있습니다.
- 스레드 풀:
concurrent.futures
모듈에서ThreadPoolExecutor
를 사용하여 스레드 풀을 만들고 스레드의 재사용성을 향상시킵니다. - 큐: 큐를 사용하여 여러 스레드 간의 작업을 조정하고 생산자-소비자 모델을 구현합니다.
- GIL 제한 방지: CPU 집약적인 작업의 경우 멀티프로세싱 및
asyncio
와 같은 다른 동시 모델을 사용하는 것이 좋습니다.
13. 객체 지향 멀티스레딩 설계
실제 응용 프로그램에서는 일반적으로 더 복잡한 문제에 직면하고 멀티스레딩과 객체 지향 설계를 결합해야합니다. 다음은 객체 지향 방식으로 멀티스레드 프로그램을 설계하는 방법을 보여주는 간단한 예입니다.
import threading import time class LeapCellWorkerThread(threading.Thread): def __init__(self, name, delay): super().__init__() self.name = name self.delay = delay def run(self): print(f"{self.name} started.") time.sleep(self.delay) print(f"{self.name} completed.") if __name__ == "__main__": thread1 = LeapCellWorkerThread("LeapCell Thread 1", 2) thread2 = LeapCellWorkerThread("LeapCell Thread 2", 1) thread1.start() thread2.start() thread1.join() thread2.join() print("Main thread continues...")
이 예제에서는 Thread
클래스에서 상속하고 스레드의 실행 논리를 정의하기 위해 run
메서드를 재정의하는 LeapCellWorkerThread
클래스를 만들었습니다. 각 스레드에는 이름과 지연 시간이 주어집니다.
14. 멀티스레딩 및 리소스 관리자
특정 리소스의 할당 및 릴리스를 관리하는 리소스 관리자를 만들어야하는 시나리오를 고려하십시오. 이때 멀티스레딩을 사용하여 리소스의 비동기 관리를 달성할 수 있습니다. 다음은 간단한 리소스 관리자의 예입니다.
import threading import time class LeapCellResourceManager: def __init__(self, total_resources): self.total_resources = total_resources self.available_resources = total_resources self.lock = threading.Lock() def allocate(self, request): with self.lock: if self.available_resources >= request: print(f"Allocated {request} LeapCell resources.") self.available_resources -= request else: print("Insufficient LeapCell resources.") def release(self, release): with self.lock: self.available_resources += release print(f"Released {release} LeapCell resources.") class LeapCellUserThread(threading.Thread): def __init__(self, name, resource_manager, request, release): super().__init__() self.name = name self.resource_manager = resource_manager self.request = request self.release = release def run(self): print(f"{self.name} started.") self.resource_manager.allocate(self.request) time.sleep(1) # 할당된 리소스로 일부 작업 시뮬레이션 self.resource_manager.release(self.release) print(f"{self.name} completed.") if __name__ == "__main__": manager = LeapCellResourceManager(total_resources=5) user1 = LeapCellUserThread("LeapCell User 1", manager, request=3, release=2) user2 = LeapCellUserThread("LeapCell User 2", manager, request=2, release=1) user1.start() user2.start() user1.join() user2.join() print("Main thread continues...")
이 예제에서 LeapCellResourceManager
클래스는 리소스의 할당 및 릴리스를 담당하고 LeapCellUserThread
클래스는 리소스를 사용하는 사용자 스레드를 나타냅니다. 잠금을 사용하여 리소스의 안전한 할당 및 릴리스가 보장됩니다.
16. 멀티스레딩의 디버깅 및 성능 분석
멀티스레딩 프로그래밍을 할 때 디버깅 및 성능 분석은 간과할 수 없는 중요한 측면입니다. Python은 멀티스레드 프로그램을 더 잘 이해하고 디버깅하는 데 도움이 되는 몇 가지 도구와 기술을 제공합니다.
멀티스레드 프로그램 디버깅
- print 문 사용: 적절한 위치에
print
문을 삽입하여 프로그램의 실행 흐름을 추적하는 데 도움이 되는 주요 정보를 출력합니다. - 로깅 모듈: Python의
logging
모듈을 사용하여 스레드의 시작, 종료 및 주요 작업을 포함하여 프로그램 런타임 중에 정보를 기록합니다. - pdb 디버거: 코드에 중단점을 삽입하고 Python의 내장 디버거
pdb
를 사용하여 대화형 디버깅을 수행합니다.
import pdb # 코드에 중단점 삽입 pdb.set_trace()
멀티스레드 프로그램의 성능 분석
- timeit 모듈 사용: 코드에 타이밍 코드를 포함하여
timeit
모듈을 사용하여 특정 작업 또는 함수의 실행 시간을 측정합니다.
import timeit def my_function(): # 테스트 할 코드 # 함수의 실행 시간 테스트 execution_time = timeit.timeit(my_function, number=1) print(f"Execution time: {execution_time} seconds")
- cProfile 모듈 사용:
cProfile
은 Python의 성능 분석 도구로, 함수 호출 및 실행 시간을 보는 데 도움이됩니다.
import cProfile def my_function(): # 테스트 할 코드 # 성능 분석 실행 cProfile.run("my_function()")
- 타사 도구 사용:
line_profiler
,memory_profiler
등과 같은 일부 타사 도구는 성능 병목 현상을 찾는 데 도움이되는 더 자세한 성능 분석 정보를 제공 할 수 있습니다.
# line_profiler 설치 pip install line_profiler # line_profiler를 사용하여 성능 분석 kernprof -l script.py python -m line_profiler script.py.lprof
17. 멀티스레딩의 안전성 및 위험
멀티스레딩 프로그래밍은 프로그램 성능을 향상시킬 수 있지만 잠재적인 보안 문제도 발생합니다. 다음은 주의해야 할 몇 가지 측면입니다.
- 스레드 안전: 공유 리소스에 대한 접근이 스레드 안전하도록 보장합니다. 이는 잠금 메커니즘, 원자적 연산 등의 수단을 통해 제어할 수 있습니다.
- 교착 상태: 잠금을 사용할 때 교착 상태의 발생에 주의해야합니다. 즉, 여러 스레드가 서로 리소스 해제를 기다려 프로그램이 실행을 계속할 수 없게됩니다.
- 리소스 누출: 멀티스레딩 프로그래밍에서는 스레드가 제대로 닫히지 않거나 잠금이 제대로 해제되지 않는 등 리소스가 제대로 해제되지 않는 상황이 쉽게 발생합니다.
- GIL 제한: CPU 집약적인 작업에서는 글로벌 인터프리터 잠금 (GIL)이 성능 병목 현상으로 이어질 수 있으며 멀티스레딩 또는 다른 동시 모델을 신중하게 선택해야합니다.
18. 다른 동시 모델 탐구
멀티스레딩은 일반적으로 사용되는 동시 프로그래밍 모델이지만 유일한 선택은 아닙니다. Python은 다음과 같은 다른 동시 모델도 제공합니다.
- 멀티프로세싱 프로그래밍:
multiprocessing
모듈을 통해 구현됩니다. 각 프로세스에는 독립적인 인터프리터와 GIL이 있으며 CPU 집약적인 작업에 적합합니다. - 비동기 프로그래밍:
asyncio
모듈을 통해 구현되며 이벤트 루프 및 코루틴을 기반으로하며 I/O 집약적인 작업에 적합하며 프로그램의 동시성을 향상시킬 수 있습니다. - 병렬 컴퓨팅:
concurrent.futures
모듈에서ProcessPoolExecutor
및ThreadPoolExecutor
를 사용하여 작업을 병렬로 실행합니다.
19. 지속적인 학습 및 연습
멀티스레딩 프로그래밍은 광대하고 복잡한 분야이며이 기사는 입문 가이드 만 제공합니다. 지속적인 학습과 연습은 멀티스레딩 프로그래밍을 심층적으로 마스터하는 열쇠입니다.
Python 공식 설명서와 관련 서적을 읽고 threading
모듈의 다양한 기능과 사용법을 깊이 이해하는 것이 좋습니다. 오픈 소스 프로젝트에 참여하고 다른 사람의 소스 코드를 읽는 것도 기술을 향상시키는 좋은 방법입니다.
21. 멀티스레딩 및 코루틴의 비동기화
최신 프로그래밍에서 비동기 프로그래밍과 코루틴은 고 동시성 시나리오를 처리하는 데 중요한 도구가되었습니다. Python은 코루틴을 통해 비동기 프로그래밍을 구현하는 asyncio
모듈을 제공합니다. 기존 멀티스레딩에 비해 비동기 프로그래밍은 많은 수의 스레드를 생성하지 않고도 많은 수의 I/O 집약적 작업을보다 효율적으로 처리 할 수 있습니다.
비동기 프로그래밍의 기초
비동기 프로그래밍은 async
및 await
키워드를 사용하여 코루틴을 정의합니다. 코루틴은 런타임 중에 일시 중지 및 다시 시작할 수있는 경량 스레드입니다.
import asyncio async def leapcell_my_coroutine(): print("Start LeapCell coroutine") await asyncio.sleep(1) print("LeapCell Coroutine completed") async def leapcell_main(): await asyncio.gather(leapcell_my_coroutine(), leapcell_my_coroutine()) if __name__ == "__main__": asyncio.run(leapcell_main())
위의 예제에서 leapcell_my_coroutine
은 코루틴이고 asyncio.sleep
은 비동기 작업을 시뮬레이션하는 데 사용됩니다. 여러 코루틴은 asyncio.gather
를 통해 동시에 실행됩니다.
비동기와 멀티스레딩 비교
- 성능: 비동기 프로그래밍은 멀티스레딩에 비해 많은 수의 I/O 집약적 작업을보다 효율적으로 처리 할 수 있습니다. 비동기 작업은 다른 작업의 실행을 차단하지 않고 I / O를 기다리는 동안 제어를 양보 할 수 있기 때문입니다.
- 복잡성: 비동기 프로그래밍은 멀티스레딩보다 작성하고 이해하기가 더 어려울 수 있으며 코루틴 및 비동기 프로그래밍 모델의 개념에 익숙해야합니다.
예제: 비동기 이미지 다운로드
다음은 비동기 프로그래밍을 사용하여 이미지 다운로드를 구현하는 간단한 예입니다.
import asyncio import aiohttp async def leapcell_download_image(session, url): async with session.get(url) as response: if response.status == 200: filename = url.split("/")[-1] with open(filename, "wb") as f: f.write(await response.read()) print(f"LeapCell Downloaded: {filename}") async def leapcell_main(): image_urls = ["url1", "url2", "url3"] # 실제 이미지 URL로 대체 async with aiohttp.ClientSession() as session: tasks = [leapcell_download_image(session, url) for url in image_urls] await asyncio.gather(*tasks) if __name__ == "__main__": asyncio.run(leapcell_main())
이 예제에서는 aiohttp
라이브러리를 통해 비동기 HTTP 요청이 작성되고 여러 코루틴이 asyncio.gather
를 통해 동시에 실행됩니다.
22. 비동기 프로그래밍의 예외 처리
비동기 프로그래밍에서는 예외를 처리하는 방법도 다릅니다. 코루틴에서는 일반적으로 try-except
블록 또는 asyncio.ensure_future
와 같은 메서드를 사용하여 예외를 처리합니다.
import asyncio async def leapcell_my_coroutine(): try: # 비동기 작업 await asyncio.sleep(1) raise ValueError("An error occurred") except ValueError as e: print(f"LeapCell Caught an exception: {e}") async def leapcell_main(): task = asyncio.ensure_future(leapcell_my_coroutine()) await asyncio.gather(task) if __name__ == "__main__": asyncio.run(leapcell_main())
이 예제에서 asyncio.ensure_future
는 코루틴을 Task
객체로 래핑합니다. await asyncio.gather
를 사용하여 작업이 완료 될 때까지 기다리면 예외가 잡힙니다.
23. 비동기 프로그래밍의 장점 및 주의 사항
장점
- 높은 동시성: 비동기 프로그래밍은 많은 수의 I/O 집약적 작업에 적합합니다. 동시 요청을보다 효율적으로 처리하고 시스템의 처리량을 향상시킬 수 있습니다.
- 리소스 효율성: 멀티스레딩에 비해 비동기 프로그래밍은 일반적으로 코루틴이 가볍고 단일 스레드에서 여러 코루틴을 실행할 수 있기 때문에 더 많은 리소스를 절약합니다.
주의 사항
- 차단 작업: 비동기 프로그래밍에서는 차단 작업이 전체 이벤트 루프, 차단 호출에 영향을 미치며 가능한 한 피해야합니다.
- 예외 처리: 비동기 프로그래밍에서 예외 처리는 더 복잡 할 수 있으며 코루틴의 예외 상황을 신중하게 처리해야합니다.
- 적용 가능한 시나리오: 비동기 프로그래밍은 CPU 집약적 작업보다는 I/O 집약적 작업에 더 적합합니다.
24. 더 많은 비동기 프로그래밍 도구 및 라이브러리 탐색
asyncio
및 aiohttp
외에도 몇 가지 강력한 비동기 프로그래밍 도구 및 라이브러리가 있습니다.
- asyncpg: 비동기 PostgreSQL 데이터베이스 드라이버입니다.
- aiofiles: 비동기 파일 작업 라이브러리입니다.
- aiohttp: 비동기 HTTP 클라이언트 및 서버 프레임 워크입니다.
- aiomysql: 비동기 MySQL 데이터베이스 드라이버입니다.
- uvloop: 표준 이벤트 루프를 대체하는 데 사용되는 고성능 이벤트 루프입니다.
25. 지속적인 학습 및 연습
비동기 프로그래밍은 광범위하고 심층적 인 주제이며이 기사는 간략한 소개 만 제공합니다. 이벤트 루프, 코루틴 및 비동기 작업과 같은 개념을 이해하기 위해 asyncio
모듈의 문서를 심층적으로 연구하는 것이 좋습니다.
동시에 실제 프로젝트를 통해 비동기 프로그래밍의 기술과 모범 사례를 더 잘 이해하고 마스터 할 수 있습니다.
결론
이 기사에서는 Python의 멀티스레딩 프로그래밍과 비동기 프로그래밍을 자세히 살펴보고 멀티스레딩 모듈 (threading
)의 기본 지식, 코드 연습은 물론 비동기 프로그래밍 모듈 (asyncio
)의 기본 개념과 사용법을 다루었습니다. Thread
클래스, 잠금 메커니즘, 스레드 안전성과 같은 멀티스레딩의 기초부터 시작하여 실용적인 응용 프로그램에서 멀티스레딩의 적용 시나리오와 주의 사항을 점차적으로 시연했습니다. 예제를 통해 멀티스레드 이미지 다운로드 프로세스를 보여 주어 스레드 안전과 예외 처리의 중요성을 강조했습니다.
Leapcell : 최고의 서버리스 웹 호스팅
마지막으로 Python 서비스를 배포하는 데 가장 적합한 플랫폼 인 **Leapcell**을 추천하고 싶습니다.
🚀 좋아하는 언어로 빌드
JavaScript, Python, Go 또는 Rust로 쉽게 개발하십시오.
🌍 무제한 프로젝트를 무료로 배포
사용한만큼만 지불하십시오. 요청이없고 요금이 부과되지 않습니다.
⚡ 사용한만큼 지불, 숨겨진 비용 없음
유휴 요금이없고 원활한 확장 성만 있습니다.
🔹 Twitter에서 팔로우하십시오 : @LeapcellHQ