배경 및 결론
- 이전 포스트에서
ProcessPoolExecutor'가 ThreadPoolExecutor
보다 압도적인 성능을 보였다. 그렇다면 ThreadPoolExecutor는 언제 사용하는게 좋은가? 결론적으로ThreadPoolExecutor
가ProcessPoolExecutor
보다 유리한 상황은 주로 I/O 바운드 작업이나 덜 CPU 집약적인 작업을 수행할 때이다.
1. I/O 바운드 작업
- 작업 특성: 파일 읽기/쓰기, 네트워크 요청, 데이터베이스 통신 등의 I/O 바운드 작업은 실제 CPU 계산보다 데이터를 읽거나 쓰는 데 시간이 더 많이 소요됩니다. 이러한 작업에서는 CPU 사용률이 높지 않기 때문에 프로세스 간의 오버헤드가 불필요할 수 있습니다.
- 효율성: 스레드는 프로세스에 비해 생성 시간과 자원 사용이 적게 듭니다. 따라서 I/O 바운드 작업을 처리할 때
ThreadPoolExecutor
를 사용하면 더 낮은 오버헤드로 빠르게 작업을 수행할 수 있습니다.
2. 자원 공유가 필요할 때
- 메모리 공유:
ThreadPoolExecutor
에서 스레드들은 같은 메모리 공간을 공유합니다. 이는 공유되어야 하는 데이터나 상태가 있을 때 유용하며, 프로세스 간 통신보다 데이터 접근과 조작이 간단하고 빠릅니다. - 데이터 동기화: 공유된 메모리를 통해 여러 스레드가 데이터를 효율적으로 동기화할 수 있습니다. 반면, 프로세스는 독립된 메모리 공간을 가지므로 데이터를 공유하려면 복잡한 IPC(프로세스 간 통신) 메커니즘이 필요합니다.
3. 경량 작업에 적합
- 빠른 설정과 해제: 스레드는 프로세스보다 생성과 종료가 빠르며 자원 소모가 적습니다. 작업이 많지만 각 작업의 수행 시간이 짧은 경우, 스레드를 이용한 병렬 처리가 더 효율적일 수 있습니다.
4. 확장성과 간단한 구현
- 구현의 용이성: Python에서 스레드를 사용하는 것은 프로세스를 사용하는 것보다 코드 상에서 간단하게 구현할 수 있습니다. 특히 복잡한 프로그램에서 스레드를 사용하면 프로세스를 사용할 때보다 구현이 간단해지고 오류 발생 가능성이 줄어듭니다.
실제 예(스크래핑에 스레드작업)
실무에서 ThreadPoolExecutor
를 사용하는 대표적인 예는 웹 스크래핑, 대량의 API 요청 처리, 파일 처리 등 다양한 I/O 바운드 작업에 있어서 병렬처리를 통해 성능을 향상시키는 경우입니다.
예제: 웹 스크래핑을 위한 ThreadPoolExecutor 사용
import concurrent.futures
import requests
import time
# 웹 페이지의 내용을 다운로드하는 함수
def fetch_url(url):
response = requests.get(url)
return response.text
# 주어진 URL 목록에 대해 웹 스크래핑을 수행하고 결과를 출력하는 함수
def run_web_scraping_tasks(urls):
print("Starting web scraping...") # 스크래핑 시작을 알림
start_time = time.time() # 스크래핑 시작 시간 기록
# 결과를 저장할 딕셔너리
results = {}
# ThreadPoolExecutor를 사용하여 웹 스크래핑을 병렬로 실행
with concurrent.futures.ThreadPoolExecutor(max_workers=5) as executor:
# 각 URL에 대한 Future 객체를 생성하고 딕셔너리에 저장
future_to_url = {executor.submit(fetch_url, url): url for url in urls}
# Future 객체가 완료될 때까지 기다리며, 각각의 결과를 처리
for future in concurrent.futures.as_completed(future_to_url):
url = future_to_url[future] # Future 객체에 해당하는 URL을 가져옴
try:
# Future 객체로부터 결과를 얻어옴
data = future.result()
# 결과 딕셔너리에 URL과 데이터를 저장
results[url] = data
print(f"Fetched {url} successfully") # 성공적으로 데이터를 가져왔음을 출력
except Exception as exc:
# 예외 발생 시 예외 메시지를 출력
print(f'{url} generated an exception: {exc}')
end_time = time.time() # 스크래핑 종료 시간 기록
print(f"All tasks completed in {end_time - start_time} seconds") # 전체 실행 시간 출력
return results # 결과 딕셔너리 반환
if __name__ == "__main__":
# 스크래핑할 URL 목록
urls = [
"http://example.com",
"http://example.org",
"http://example.net"
]
# 스크래핑 작업 실행
run_web_scraping_tasks(urls)
코드 설명
fetch_url
함수: 주어진 URL에서 웹 페이지의 콘텐츠를 다운로드합니다.requests
라이브러리를 사용하여 HTTP 요청을 수행합니다.run_web_scraping_tasks
함수: 주어진 URL 리스트에 대해ThreadPoolExecutor
를 사용하여fetch_url
함수를 병렬로 실행합니다. 각 URL에 대한 요청이 완료되는 대로 결과를 수집하고, 성공적으로 데이터를 가져온 경우와 예외가 발생한 경우를 출력합니다.- 성능 측정: 작업 시작 시간과 종료 시간을 기록하여, 모든 작업의 완료 시간을 계산합니다.
여기서 ThreadPoolExecutor가 더 효과적인 이유
- 리소스 사용과 관리
- 오버헤드:
ThreadPoolExecutor
는 스레드를 사용하여 작업을 수행하기 때문에 프로세스를 생성하고 관리하는 데 드는 오버헤드가 훨씬 적습니다. 스레드는 프로세스보다 메모리와 시작 시간이 적게 소모됩니다. - 메모리 공유: 스레드는 같은 메모리 공간을 공유하기 때문에, 작업 간 메모리 접근이 더 빠르고 효율적입니다. 프로세스는 각각 독립된 메모리 공간을 가지므로, 데이터를 공유하려면 상대적으로 복잡하고 비용이 많이 드는 통신 방법이 필요합니다.
- I/O 바운드 작업의 특성
- CPU 사용률: 웹 스크래핑과 같은 I/O 바운드 작업은 CPU 자원을 많이 사용하지 않고, 대부분 네트워크 요청이나 디스크 I/O에 시간을 소모합니다. 이러한 작업에서는 스레드가 I/O 작업 완료를 기다리는 동안 CPU가 다른 스레드로 전환할 수 있어 효율적입니다.
- 병렬성: 스레드를 사용하면 여러 I/O 작업을 병렬로 처리할 수 있으며, 각 스레드가 I/O 작업을 기다리는 동안 다른 스레드가 CPU를 사용할 수 있어 자원 활용도가 높습니다.
- 실용성과 간편성
- 구현의 용이성: 스레드를 사용하는 코드는 프로세스를 사용하는 코드보다 일반적으로 더 간단하고 이해하기 쉽습니다. Python의 스레드는 특히 글로벌 인터프리터 락(GIL) 때문에 CPU 바운드 작업에는 적합하지 않지만, I/O 바운드 작업에서는 GIL의 영향을 크게 받지 않습니다.
결론
- 웹 스크래핑과 같은 I/O 바운드 작업에서는
ThreadPoolExecutor
가ProcessPoolExecutor
보다 일반적으로 더 효과적입니다. 리소스 사용이 적고, 구현이 간단하며, I/O 작업의 병렬 처리에 있어서 높은 효율을 제공합니다. 프로세스 기반의 병렬 처리는 독립된 메모리 공간을 요구하고 오버헤드가 더 크기 때문에, 주로 CPU 바운드 작업에 더 적합합니다.
예제2: 공유 카운터 업데이트
import concurrent.futures
import threading
# 공유 리소스
counter = 0
lock = threading.Lock()
def increment_counter():
global counter
with lock: # 락을 사용하여 스레드간 안전하게 접근
for _ in range(10000):
counter += 1
def run_shared_counter_tasks():
workers = 5
print(f"Starting counter with {workers} workers...")
with concurrent.futures.ThreadPoolExecutor(max_workers=workers) as executor:
# 모든 스레드에서 카운터 증가 작업을 실행
executor.map(increment_counter, range(workers))
print(f"Final counter value: {counter}")
if __name__ == "__main__":
run_shared_counter_tasks()
코드 설명
- 메모리 공유: 모든 스레드가 counter 변수를 공유합니다. 이 변수는 모든 스레드에 의해 동시에 접근되고 수정될 수 있으므로, 스레드 안전한 방식으로 접근해야 합니다.
- 락 사용: threading.Lock()을 사용하여 counter 변수에 대한 접근을 동기화합니다. 이는 레이스 컨디션을 방지하고 데이터 일관성을 유지
- ThreadPoolExecutor 사용: 스레드 풀을 사용하여 정의된 수의 스레드에서 동시에 카운터 증가 함수를 실행합니다. 이를 통해 병렬성을 제공하면서도 공유 메모리에 안전하게 접근할 수 있습니다.
공유 카운터 응용
- 리소스 액세스 제한
웹 서버나 애플리케이션에서 동시에 접근할 수 있는 최대 사용자 수를 제한하기 위해 사용될 수 있습니다. 예를 들어, 특정 서비스나 데이터베이스에 대한 동시 접근 수를 제한하려고 할 때, 공유 카운터를 통해 현재 접근 중인 사용자 수를 추적하고, 이 수치가 특정 임계값을 초과하지 않도록 관리할 수 있습니다. - 트랜잭션 관리
데이터베이스 관리 시스템에서 트랜잭션의 숫자를 관리하는 데 사용될 수 있습니다. 여러 트랜잭션이 동시에 발생할 때 각 트랜잭션의 진행 상태를 카운트하여 시스템이 오버로드되는 것을 방지합니다. - 작업 분배
분산 시스템에서 작업을 여러 노드에 균등하게 분배하기 위해 사용됩니다. 공유 카운터를 활용하여 각 노드가 처리하는 작업 수를 모니터링하고, 이를 기반으로 새로운 작업을 할당함으로써 부하를 균형 있게 조절할 수 있습니다. - 멀티스레드 애플리케이션의 성능 모니터링
복잡한 멀티스레드 애플리케이션에서 각 스레드의 작업 처리 횟수를 카운트하여 애플리케이션의 성능을 모니터링하고 분석합니다. 이 데이터는 시스템의 병목 현상을 식별하고 성능 최적화를 위한 중요한 인사이트를 제공할 수 있습니다.
그외 실무에서 사용하는 예
1. 웹 스크래핑
웹 스크래핑 작업은 일반적으로 많은 수의 HTTP 요청을 병렬로 실행해야 합니다. ThreadPoolExecutor
는 여러 웹 페이지의 데이터를 동시에 다운로드하고 처리하는 데 사용되어, 전체 작업의 완료 시간을 대폭 줄일 수 있습니다.
2. 데이터베이스 배치 처리
대용량 데이터를 처리할 때 여러 쿼리를 동시에 실행해야 할 경우가 많습니다. ThreadPoolExecutor
를 사용하여 다수의 데이터베이스 쿼리를 병렬로 실행함으로써 응답 시간을 단축하고 시스템의 처리량을 향상시킬 수 있습니다.
3. API 요청 처리
서버가 외부 API로부터 데이터를 가져오는 경우, 여러 API 호출을 동시에 수행하여 네트워크 지연 시간의 영향을 최소화할 수 있습니다. ThreadPoolExecutor
는 API 응답을 기다리는 동안 다른 작업을 계속 처리할 수 있어 효율적입니다.
4. 파일 입출력 작업
파일 시스템에서 대량의 파일을 읽고 쓰는 작업을 수행할 때, ThreadPoolExecutor
를 이용하여 여러 파일에 대한 입출력 작업을 병렬로 처리할 수 있습니다. 이는 특히 대용량 로그 파일이나 데이터 파일을 처리할 때 유용합니다.
5. 실시간 데이터 처리
실시간으로 수집되는 데이터(예: 센서 데이터, 금융 거래 데이터 등)를 처리하는 시스템에서 ThreadPoolExecutor
를 사용하여 데이터를 신속하게 처리하고 분석할 수 있습니다. 이 방법은 데이터의 병목 현상을 방지하고, 시스템의 반응성을 향상시킬 수 있습니다.
결론
- ThreadPoolExecutor와 ProcessPoolExecutor의 용도의 차이에 대해 알수 있었다.
- 다음에는 스레드,프로세서가 무슨 구조적 차이가 있길래 이런 차이가 나오는지 알아보기로 한다.
'그때그때 CS 정리' 카테고리의 다른 글
나만의 코드 규칙 정립 (0) | 2024.06.30 |
---|---|
스레드와 프로세서의 구조적 차이 (0) | 2024.06.20 |
파이썬 병렬 실행 연구 (0) | 2024.06.20 |
비동기 프로그래밍(asyn,aiohttp, aiofiles,aiomysql)은 뭘까? (0) | 2024.06.19 |
벡터 데이터베이스 (0) | 2024.06.18 |