Python 병렬 처리 : GIL, threading, asyncio, multiprocessing
목차2. GIL(Global Interpreter Lock) 개념
5. Multiprocessing의 장단점 및 사용 예시
1. 병렬처리란?
병렬 처리(Parallel Processing)란 여러 작업을 동시에 실행시켜 전체 작업의 처리 속도를 높이는 기법입니다.
현대 컴퓨터는 멀티코어 CPU와 고속 I/O 장치를 제공하기 때문에, 이 성능을 제대로 활용하려면 병렬 처리 구조가 필수적입니다.
Python에서는 주로 아래와 같은 병렬/동시 처리 방식이 사용됩니다:
- threading: OS 수준의 스레드 사용
- asyncio: 싱글 스레드 기반 비동기 I/O 처리
- multiprocessing: 프로세스 기반의 병렬 처리
Python에서는 위의 대표적인 방식 외에도 다음과 같은 고급 병렬 처리 기법들이 존재합니다.
- concurrent.futures (고수준 API) : ThreadPoolExecutor, ProcessPoolExecutor 제공
- joblib (데이터 과학 특화 병렬 처리) : scikit-learn, numpy 기반 병렬 처리 최적화, 대규모 반복 작업에 강
- GPU 기반 병렬 처리 (CUDA, CuPy, Numba) : 이미지 처리, 행렬 계산 등 대규모 연산에서 성능 극대화
2. GIL(Global Interpreter Lock) 개념
GIL(Global Interpreter Lock)은 Python의 CPython 인터프리터에서 동시성 문제를 방지하기 위해 사용하는 락(Lock)입니다.
"한 번에 하나의 스레드만 Python 바이트코드를 실행할 수 있도록 강제" 하는 일종의 장치입니다.
- Python은 내부적으로 객체 참조 횟수(Reference Count)를 통해 메모리를 관리합니다.
- 하지만 여러 스레드가 동시에 객체에 접근해 참조 카운트를 수정하면 충돌(Race Condition)이 발생할 수 있습니다.
- 이를 막기 위해 CPython은 GIL을 도입해 하나의 스레드만 실행을 허용하여 메모리 안전성을 확보한 것입니다.
▶ CPU 바운드 작업에서는 성능 저하
- 여러 개의 threading.Thread를 만들어도 GIL이 한 번에 하나만 실행하기 때문에,
- 멀티코어 CPU 환경에서도 하나의 코어만 사용
- → 오히려 스레드 간 전환(context switching) 오버헤드로 성능 저하 발생 가능
▶ I/O 바운드 작업에서는 큰 영향 없음
- 네트워크 대기, 파일 읽기 등 I/O 대기 시간이 많은 작업에서는 GIL이 잠시 해제됨
- 다른 스레드가 이 시점에 실행될 수 있으므로 동시 처리 성능 향상 가능
GIL 때문에 파이썬 쓰레드는 한 쓰레드에서 CPU점유를 마칠때까지 기다렸다가 다음 쓰레드가 동작한다.
그러므로 I/O가 많고 CPU Idle이 많은 경우에 좋은 성능을 발휘하고, 각 쓰레드 당 작업이 CPU작업이 많을 경우 성능이 느려진다.
병령처리방식 | GIL영샹 | 설명 |
threading | ✅ 있음 | 동시에 여러 스레드 실행 불가 (Python 코드에 한정) |
asyncio | ✅ 있음 (하지만 비동기 방식이므로 영향 적음) | 싱글 스레드로 스케줄링, GIL에 크게 의존하지 않음 |
multiprocessing | ❌ 없음 | 프로세스 별로 별도 Python 인터프리터 사용 → 병렬 가능 |
3. Threading의 장단점 및 사용 예시
스레드(Thread)는 하나의 프로세스 내에서 실행되는 가벼운 실행 단위입니다.
Python에서 threading 모듈을 사용하면 여러 작업을 동시에 실행(동시성)할 수 있습니다.
특히 파일 다운로드, 웹 요청, 데이터 저장 등 입출력(I/O) 중심의 작업에 적합합니다.
▶ Thread의 장단점
threading은 I/O 바운드 작업에는 적합하지만, CPU 바운드 작업에는 성능상 제약이 있다는 점이 핵심입니다.
항목 | 장점 | 단점 |
I/O 바운드 처리 | 네트워크, 파일 입출력 등에서 높은 효율 | CPU 계산에는 효과 없음 (GIL) |
코드 작성 난이도 | 구조가 간단하고 직관적, 함수만 있으면 바로 구현 가능 | 동기화 로직이 필요해 복잡해질 수 있음 |
리소스 사용량 | 프로세스보다 메모리 사용이 적고 생성 속도 빠름 | 스레드 수가 많아지면 context switching 오버헤드 발생 |
데이터 공유 | 동일 프로세스 내에서 전역 변수 등 쉽게 공유 가능 | 동시 접근 시 Race Condition 발생 위험 |
병렬 처리 성능 | I/O 작업은 GIL 영향이 적어 효율적 동시 처리 가능 | CPU 바운드 작업은 GIL로 인해 병렬 처리 불가 |
적합한 용도 | 웹 크롤링, 파일 다운로드, DB 질의 등 I/O 중심 작업 | 과학 계산, 이미지 처리 등 고연산 작업은 부적합 |
▶ 다중 다운로드 시뮬레이션 예제:
import threading
import time
def download_file(index):
print(f"📥 파일 {index} 다운로드 시작")
time.sleep(2) # 실제 다운로드 대신 대기 시간으로 시뮬레이션
print(f"✅ 파일 {index} 다운로드 완료")
# 스레드 목록 저장용 리스트
threads = []
# 3개의 파일을 동시에 다운로드
for i in range(3):
t = threading.Thread(target=download_file, args=(i,))
t.start()
threads.append(t)
# 모든 스레드가 종료될 때까지 대기
for t in threads:
t.join()
print("📁 모든 다운로드 완료")
📥 파일 0 다운로드 시작
📥 파일 1 다운로드 시작
📥 파일 2 다운로드 시작
✅ 파일 0 다운로드 완료
✅ 파일 1 다운로드 완료
✅ 파일 2 다운로드 완료
📁 모든 다운로드 완료
▶ 공유 자원 접근 예제 (Lock 사용) :
import threading
count = 0
lock = threading.Lock()
def increment():
global count
for _ in range(100000):
with lock: # 동기화 블록
count += 1
threads = [threading.Thread(target=increment) for _ in range(5)]
for t in threads: t.start()
for t in threads: t.join()
print(f"최종 count 값: {count}")
- ✅ 위처럼 Lock을 사용하면 Race Condition을 방지할 수 있습니다.
- ❌ Lock 없이 실행하면 결과 값이 올바르지 않을 수 있습니다.
▶ Thread 클래스 상속 사용
class MyThread(threading.Thread):
def __init__(self, name):
super().__init__()
self.name = name
def run(self):
print(f"🔄 {self.name} 시작")
time.sleep(1)
print(f"✅ {self.name} 완료")
threads = [MyThread(f"작업-{i}") for i in range(3)]
for t in threads: t.start()
for t in threads: t.join()
▶ ThreadPoolExecutor 구현예제:
from concurrent.futures import ThreadPoolExecutor, as_completed
import time
def download_file(index):
print(f"📥 파일 {index} 다운로드 시작")
time.sleep(2)
print(f"✅ 파일 {index} 다운로드 완료")
return f"파일 {index}"
# 스레드 풀 생성 및 작업 제출
with ThreadPoolExecutor(max_workers=3) as executor:
futures = [executor.submit(download_file, i) for i in range(3)]
# 작업 완료 대기 및 결과 수집
for future in as_completed(futures):
result = future.result() # 필요 시 예외 처리 가능
print(f"📄 완료된 작업: {result}")
print("📁 모든 다운로드 완료")
4. Asyncio의 장단점 및 사용 예시
asyncio는 Python에서 비동기 프로그래밍을 지원하는 표준 라이브러리입니다.
하나의 스레드와 하나의 이벤트 루프(Event Loop)를 사용해 수많은 작업을 동시 처리(concurrency)할 수 있습니다.
▶ Asyncio의 장단점
구분 | 장점 | 단점 |
동시성 처리 성능 | 수천 개의 작업을 싱글 스레드에서 동시에 처리 가능 | CPU 바운드 작업에서는 성능 저하 및 이벤트 루프 블로킹 발생 |
자원 효율성 | 스레드/프로세스 사용 없이 낮은 메모리 사용 | await를 누락하면 전체 흐름이 중단될 수 있음 (주의 필요) |
GIL 영향 | GIL이 있어도 I/O 작업은 비동기 전환으로 높은 성능 발휘 | GIL은 여전히 존재 → CPU 연산 병렬 불가 |
코드 표현력 | async/await 문법으로 비동기 흐름이 명확하고 가독성 좋음 | 초기 학습 곡선 있음 (coroutine, task, loop 구조 이해 필요) |
스케일링 | 웹 서버, 크롤러, 백그라운드 작업 등에서 확장성 우수 | 동기 코드와 혼합 시 디버깅이 복잡해질 수 있음 |
표준 지원 | Python 3.4+ 내장 표준 라이브러리 (asyncio) | 일부 서드파티 라이브러리는 비동기 지원이 부족할 수 있음 |
▶ async, await, async def, gather(), create_task()
키워드 | 역할 |
async def | 비동기 함수 정의 |
await | 비동기 함수 또는 비동기 작업 실행을 기다림 |
async | def 앞에 붙여서 코루틴 정의 / with, for에도 사용 |
gather() | 여러 코루틴을 동시에 실행하고 결과를 묶어서 반환 |
create_task() | 코루틴을 백그라운드 태스크로 예약, 곧바로 실행 시작 |
import asyncio
async def task(name, sec):
print(f"{name} 시작")
await asyncio.sleep(sec)
print(f"{name} 완료")
return f"{name} 결과"
async def main():
# create_task로 비동기 실행 예약
t1 = asyncio.create_task(task("작업1", 2))
t2 = asyncio.create_task(task("작업2", 1))
# 동시에 실행 후 결과 기다림
result = await asyncio.gather(t1, t2)
print(result)
asyncio.run(main())
▶ 다중 다운로드 시뮬레이션 예제:
import asyncio
async def download_file(index, delay):
print(f"📥 파일 {index} 다운로드 시작 (지연: {delay}s)")
await asyncio.sleep(delay)
print(f"✅ 파일 {index} 다운로드 완료")
return f"파일 {index}"
async def main():
tasks = [asyncio.create_task(download_file(i, d)) for i, d in enumerate([3, 1, 2])]
for task in asyncio.as_completed(tasks):
result = await task
print(f"📄 완료된 작업: {result}")
asyncio.run(main())
▶ asyncio 사용 제안
유형 | 예시 |
대규모 I/O 작업 | 웹 크롤러, API 대량 호출 |
네트워크 서버 | 채팅, 실시간 게임 서버 |
GUI 이벤트 루프와 통합 | Qt, Tkinter 비동기 연동 |
빠른 응답성과 높은 동시성 필요 시 | ✅ asyncio가 최적 |
▶ threading과 asyncio 비교
항목 | threading | asyncio |
구조 | OS 스레드 기반 | 이벤트 루프 기반 |
스레드 수 | 많을수록 context switching 비용 발생 | 스레드 1개만 사용 |
I/O 바운드 처리 | 가능 (GIL 영향 적음) | ✅ 최적 |
CPU 바운드 처리 | ❌ 성능 떨어짐 | ❌ 사용 불가 |
코드 난이도 | 비교적 쉬움 | 약간 복잡 (초기 러닝커브 있음) |
예외 처리 | 직접 try/except | gather(), create_task() 등으로 안전하게 관리 |
5. Multiprocessing의 장단점 및 사용 예시
Python의 multiprocessing 모듈은 여러 개의 프로세스를 생성하여 병렬 실행할 수 있도록 해주는 표준 라이브러리입니다.
각 프로세스는 독립적인 메모리 공간과 Python 인터프리터를 가지기 때문에,
GIL(Global Interpreter Lock)의 제약을 받지 않고 진정한 병렬 처리(Parallelism)가 가능합니다.
CPU 바운드 작업(수학 계산, 이미지 처리 등)에 매우 적합합니다.
▶ Multiprocessing 의 장단점
구분 | 장점 | 단점 |
병렬 처리 성능 | GIL 없이 진짜 병렬 처리(Parallelism) 가능 | 프로세스 생성/종료에 시간과 자원 비용이 큼 |
멀티코어 활용 | CPU 코어를 모두 사용하여 성능 극대화 | 코어 수를 초과하면 오히려 성능 저하 가능성 있음 |
CPU 바운드 작업 | 이미지 처리, 수치 계산, AI 연산 등 고성능 처리 가능 | I/O 중심 작업에는 비효율적 |
자원 분리 | 각 프로세스가 독립된 메모리 공간 사용 → 충돌 방지 | 데이터 공유 복잡 (Queue, Pipe, Manager 등 필요) |
표준 라이브러리 포함 | multiprocessing, ProcessPoolExecutor로 지원 | 디버깅과 예외 처리 어려움, 에러 추적 복잡 |
GIL 영향 없음 | CPython의 GIL 완전 회피 | 함수나 객체가 직렬화(Pickle) 가능해야 함 |
호환성 | 대부분 OS에서 동작 | Windows에서는 반드시 if __name__ == '__main__' 필요 |
▶ 숫자 계산 병렬 실행 예제:
- CPU 코어 수만큼 프로세스를 병렬 실행 → 실제 병렬성(Parallelism) 보장
from multiprocessing import Process
import time
def compute(n):
print(f"[{n}] 시작")
total = sum(i * i for i in range(10_000_000))
print(f"[{n}] 완료")
if __name__ == '__main__':
processes = []
for i in range(4): # 4개의 프로세스 생성
p = Process(target=compute, args=(i,))
p.start()
processes.append(p)
for p in processes:
p.join()
print("✅ 모든 프로세스 완료")
▶ ProcessPoolExecutor로 병렬 처리 예제:
- multiprocessing 기반이지만, 고수준 API로 간편하게 병렬 처리 가능
- with 블록 사용으로 프로세스 관리 자동화
from concurrent.futures import ProcessPoolExecutor
def task(n):
return sum(i * i for i in range(10_000_000)) + n
if __name__ == '__main__':
with ProcessPoolExecutor(max_workers=4) as executor:
results = executor.map(task, range(4))
for r in results:
print(f"✅ 결과: {r}")
6. Threading vs Asyncio vs Multiprocessing 비교
항목 | multiprocessing | threading | asyncio |
병렬성 | ✅ 진짜 병렬 실행 | ❌ GIL로 제약 | ❌ 싱글 스레드 (I/O 병렬) |
CPU 작업 | ✅ 매우 적합 | ❌ 오히려 느림 | ❌ 비적합 |
I/O 작업 | ❌ 오버헤드 큼 | ✅ 적합 | ✅ 최적 |
메모리 공유 | ❌ 불가능 (직렬화 필요) | ✅ 가능 | ✅ 가능 (동기화 필요 없음) |
프로세스/스레드 수 | 제한적 (비쌈) | 수백 개 가능 | 수천 개 가능 |
예외/디버깅 | 어려움 | 쉬움 | 보통 |
플랫폼 제약 | Windows에서는 __main__ 블록 필수 | 없음 | 없음 |
작업유형 | 추천 | 활용분야 |
CPU 바운드 작업 | multiprocessing | 이미지 처리, 수치 계산, 모델 학습 등 |
I/O 바운드 + 간단한 구조 | threading | 파일 다운로드, 소켓 요청, 병렬 API 호출 등 |
I/O 바운드 + 대규모 동시성 | asyncio | 웹 크롤러, 실시간 서버, 비동기 API 클라이언트 등 |
관련 글 링크
5. Python 예외(Exception) 처리 : try-except-finally, with
5. Python 예외(Exception) 처리 : try-except-finally, with
예외 처리는 프로그램의 안정성과 복원력을 높이는 데 핵심적인 역할을 합니다. 하지만 try-except 구문을 잘못 사용하면 오히려 버그를 숨기거나 성능 저하를 유발할 수 있습니다. Python 예외(Except
quadcube.tistory.com
4. Python 함수구조, 함수인자, 람다함수, 클로저, 고차함수, 데코레이터
4. Python 함수구조, 함수인자, 람다함수, 클로저, 고차함수, 데코레이터
Python 함수구조, 함수인자, 람다함수, 클로저, 고차함수, 데코레이터 목차 1. 함수 구조 2. 함수의 인자(Arguments) 3. 전역변수와 지역변수 4. 람다함수(Lambda function) 5. 클로저(Closure) 6. 고차 함수(High-Or
quadcube.tistory.com
3. Python 클래스 정리: 클래스, 상속, 메서드, 접근제어
3. Python 클래스 정리: 클래스, 상속, 메서드, 접근제어
Python 클래스의 기본 구조부터 생성자, 메서드, 상속, 소멸자까지 핵심 개념을 정리했습니다.실무에 바로 적용 가능한 예제와 함께 __init__, self, __del__, __enter__, __exit__까지 전체 흐름을 이해할 수
quadcube.tistory.com
2. Python 변수 정리: 지역변수, 전역변수, global, 클래스 변수
2. Python 변수 정리: 지역변수, 전역변수, global, 클래스 변수
Python 변수 정리: 지역변수, 전역변수, global, 클래스 변수 목차 1. 지역변수 vs 전역변수 2. global 키워드의 역할과 주의사항 3. nonlocal 키워드 4. 변수처럼 다루는 함수-일급객체로서의 함수 5. 클래스
quadcube.tistory.com
'3.SW개발 > Python' 카테고리의 다른 글
7. Python 자료형 정리 : List, Tuple, Dictionary, Set, Sequence, Range (3) | 2025.05.23 |
---|---|
5. Python 예외(Exception) 처리 : try-except-finally, with (0) | 2025.05.22 |
4. Python 함수구조, 함수인자, 람다함수, 클로저, 고차함수, 데코레이터 (0) | 2025.05.22 |
3. Python 클래스 정리: 클래스, 상속, 메서드, 접근제어 (0) | 2025.05.21 |
2. Python 변수 정리: 지역변수, 전역변수, global, 클래스 변수 (0) | 2025.05.21 |