728x90
한 번에 여러 가지 일을 처리하게 하는 방법... -> 동시성 or 병렬 프로그래밍
간단한 용어 정리
- 동시성: 대기 중인 여러 태스크를 한 번에 하나씩(가능하면 병렬로) 진행해서 모든 태스크를 처리하는 능력
- 병렬성: 여러 계산을 동시에 실행하는 능력
- 실행 유닛: 코드를 동시에 실행하는 객체를 일컫는 포괄적 용어.
- 프로세스 실행 중인 프로그램의 인스턴스이며, 메모리와 CPU 시간 슬라이스 사용. 메모리 분리된 독립 실행 단위. 선점형 멀티태스킹 허용
- 스레드: 프로세스 안에 있는 하나의 실행 유닛. 프로세스 내에서 동일 메모리 공유
- 코루틴: 멈췄다가 나중에 다시 실행 재개할 수 있는 함수. 협업형 멀티태스킹 지원.
- 큐: 선입 선출 방식의 데이터 구조. 안전하게 데이터 교환
- 락: 실행 유닛이 작업을 동기화하고 데이터 훼손 방지하는 데에 사용하는 객체
- 경쟁: 락 없이 여러 스레드 / 코루틴이 제한된 자원에 접근하려고 할 때 발생
파이썬에 적용
- 동시성:
async
/await
, 코루틴, 이벤트 루프로 구현함 (asyncio
) - 병렬성:
multiprocessing
모듈로 CPU 여러 개를 써서 동시에 실행 - 실행 유닛: 보통 "코어",
multiprocessing
은 각 코어에서 병렬 실행 - 프로세스:
multiprocessing.Process
- 메모리 분리된 독립 실행 단위 - 스레드:
threading.Thread
- 같은 메모리 공유, GIL(Global Interpreter Lock) 영향 받음 - 코루틴:
async def
함수로 정의,await
로 실행 시점 제어 - 큐:
queue.Queue
(스레드용),multiprocessing.Queue
(프로세스용) - 안전하게 데이터 주고받기 - 락:
threading.Lock
,asyncio.Lock
- 동시에 접근하지 못하게 막음 - 경쟁: 락 없이 여러 스레드 / 코루틴이 같은 변수를 동시에 바꾸면 발생
GIL (Global Interperter Lock)
파이썬 인터프리터가 한 번에 하나의 스레드만 실행하게 하는 락
- 왜 만듦?
- 파이썬의 메모리 관리(특히 reference counting)가 스레드 안전하지 않기 때문
- 단순성과 안정성을 위해 만든 설계
- 문제점
threading.Thread
로 스레드를 여러 개 만들어도 CPU 연산은 항상 하나의 스레드만 실행됨- 그래서 멀티코어 성능 못 씀 -> 병렬 계산에는 부적합
import threading
import time
def count():
x = 0
for _ in range(10**7):
x += 1
start = time.time()
threads = [threading.Thread(target=count) for _ in range(2)]
[t.start() for t in threads]
[t.join() for t in threads]
print(f"time: {time.time() - start:.2f} sec") # 거의 단일 스레드 속도
GIL 때문에 2배 빠르지 X
해결 방안
- CPU 바운드 작업:
multiprocessing
,numba
,C/C++
연동 - I/O 바운드:
threading
,asyncio
- CPU 바운드 작업:
요약
- 스레드는 가볍지만 GIL 때문에 병렬 CPU 사용이 안 됨
- 프로세스는 무겁지만 GIL의 영향을 받지 않음
- CPU 연산에는
multiprocessing
, I/O에는threading/asyncio
3대 동시성 프로그래밍 모델을 이용한 스피너 구현해보기
slow()
라는 느린 작업 도중 띄우는 스피너 가정
스레드 기반
import threading, time, sys
def spinner():
while not done:
for ch in '|/-\\':
print(f'\r{ch}', end='', flush=True)
time.sleep(0.1)
def slow():
time.sleep(3) # 무거운 I/O 작업이라 가정
done = False
t = threading.Thread(target=spinner)
t.start()
slow()
done = True
t.join()
print('\rDone!')
- 간단하고 I/O 작업엔 적합
- GIL 때문에 병렬 처리 불가
프로세스 기반
from multiprocessing import Process, Value
import time, sys
def spinner(shared_flag):
while not shared_flag.value:
for ch in '|/-\\':
print(f'\r{ch}', end='', flush=True)
time.sleep(0.1)
def slow():
time.sleep(3)
if __name__ == '__main__':
flag = Value('b', False) # 공유 변수
p = Process(target=spinner, args=(flag,))
p.start()
slow()
flag.value = True
p.join()
print('\rDone!')
- GIL 우회 가능, CPU 연산도 병렬 가능
- 자원 더 많이 씀, 구조 복잡함
비동기 코루틴 기반
import asyncio
async def spinner():
while not done:
for ch in '|/-\\':
print(f'\r{ch}', end='', flush=True)
await asyncio.sleep(0.1)
async def slow():
await asyncio.sleep(3)
done = False
async def main():
global done
spin = asyncio.create_task(spinner())
await slow()
done = True
await spin
print('\rDone!')
asyncio.run(main())
- 싱글 스레드로 동시성 가능, 깔끔한 비동기 처리
- CPU 연산 병렬 불가,
await
가능한 작업만 사용 가능
프로세스 풀
여러 개의 작업을 병렬로 처리할 때 일일이 Process 객체를 만들고 관리하는 번거로움을 줄여주는 자동화된 병렬 처리 도구
multiprocessing.Process
를 직접 여러 개 만드는 대신- 프로세스를 미리 만들어 풀(pool)로 관리
- 거기에 작업 나눠서 던져주면 자동으로 분배해서 병렬 실행
from concurrent.futures import ProcessPoolExecutor
import time
def slow_task(n):
time.sleep(1)
return n * n
if __name__ == '__main__':
with ProcessPoolExecutor(max_workers=4) as executor:
results = executor.map(slow_task, range(10))
print(list(results))
max_workers=4
최대 4개 프로세스 동시에 사용range(10)
10개의 작업 자동 분배excutor.map()
은 병렬for
라고 보면 됨
References
-『 전문가를 위한 파이썬(2판) 』, 루시아누 하말류, 한빛미디어, Part 1 - Chapter 19
728x90
'CS' 카테고리의 다른 글
[Python] 비동기 프로그래밍 (2) | 2025.06.01 |
---|---|
[Python] 동시 실행자 (0) | 2025.05.18 |
[Python] with, match, else 블록 (0) | 2025.05.04 |
[Python] 반복자, 제너레이터, 고전적인 코루틴 (0) | 2025.05.03 |
[Python] 데코레이터는 언제 실행되나요? - import (1) | 2025.04.20 |