
스레드는 프로세스 내부의 실행 흐름으로, 가벼운 프로세스라고도 불립니다.
스레드는 프로세스 내에서 실행되는 독립적인 실행 흐름입니다.
쉬운 비유:
프로세스 = 식당
스레드 = 식당 직원
단일 스레드 식당:
- 주방장 1명
- 혼자서 주문 받고, 요리하고, 서빙
- 손님이 많으면 느림
멀티 스레드 식당:
- 주방장 3명
- 같은 주방(메모리) 공유
- 같은 재료(자원) 공유
- 동시에 여러 요리 가능
- 빠름!
핵심:
같은 공간과 자원을 공유하면서 독립적으로 일함
프로세스:
┌─────────────────────────────────┐
│ 프로세스 │
│ 메모리 (공유): │
│ ┌───────────────────────────┐ │
│ │ 코드 영역 │ │
│ ├───────────────────────────┤ │
│ │ 데이터 영역 (전역 변수) │ │
│ ├───────────────────────────┤ │
│ │ 힙 (동적 할당) │ │
│ └───────────────────────────┘ │
│ 스레드별 독립: │
│ ┌─────────┐ ┌─────────┐ │
│ │스레드 1 │ │스레드 2 │ │
│ │ │ │ │ │
│ │스택 │ │스택 │ │
│ │레지스터 │ │레지스터 │ │
│ │PC │ │PC │ │
│ └─────────┘ └─────────┘ │
└─────────────────────────────────┘
공유: 코드, 데이터, 힙
독립: 스택, 레지스터, PC (프로그램 카운터)
* 프로그램 카운터(Program Counter, PC) : CPU 내부 레지스터 중 하나로,
다음에 실행할 명령어의 주기억장치 주소를 가리키는 포인터
프로세스:
- 독립적인 실행 단위
- 자신만의 메모리 공간
- 무거움 (생성/전환 비용 큼)
- 안전함 (격리됨)
스레드:
- 프로세스 내부의 실행 흐름
- 메모리 공간 공유
- 가벼움 (생성/전환 비용 작음)
- 위험함 (공유로 인한 문제 가능)
def process_vs_thread_comparison():
"""
프로세스 vs 스레드 비교
"""
# ===== 메모리 =====
"""
프로세스:
- 독립적인 메모리 공간
- 프로세스 A의 변수를 B가 접근 불가
- 안전하지만 통신 어려움
스레드:
- 같은 메모리 공간 공유
- 스레드 A의 전역 변수를 B가 접근 가능
- 통신 쉽지만 동기화 필요
"""
# ===== 생성 비용 =====
"""
프로세스:
- 생성: 느림 (새 메모리 공간 할당)
- 전환: 느림 (컨텍스트 스위칭 비용 큼)
* Context Switching: CPU가 현재 작업(프로세스/스레드)을 중단하고
다른 작업으로 전환하기 위해, 이전 작업의 상태(문맥)를 저장하고
새 작업의 상태를 불러오는 핵심 기술
스레드:
- 생성: 빠름 (메모리 공유)
- 전환: 빠름 (같은 주소 공간)
"""
# ===== 통신 =====
"""
프로세스 간 통신 (IPC):
- 파이프, 소켓, 공유 메모리 등 필요
- 복잡하고 느림
스레드 간 통신:
- 전역 변수, 힙 공유
- 간단하고 빠름
"""
# ===== 안전성 =====
"""
프로세스:
- 한 프로세스가 죽어도 다른 프로세스 무관
- 격리되어 안전
스레드:
- 한 스레드가 죽으면 전체 프로세스 죽음
- 공유로 인한 버그 가능
"""
┌──────────────┬─────────────┬─────────────┐
│ 구 분 │ 프로세스 │ 스레드 │
├──────────────┼─────────────┼─────────────┤
│ 메모리 │ 독립 │ 공유 │
│ 생성 속도 │ 느림 │ 빠름 │
│ 전환 속도 │ 느림 │ 빠름 │
│ 통신 │ 어려움(IPC) │ 쉬움(공유) │
│ 안전성 │ 높음 │ 낮음 │
│ 자원 사용 │ 많음 │ 적음 │
│ 디버깅 │ 쉬움 │ 어려움 │
└──────────────┴─────────────┴─────────────┘
언제 사용?
프로세스: 독립성, 안정성 중요
스레드: 속도, 자원 효율 중요
import time
import threading
# ===== 단일 스레드 =====
def single_thread_example():
"""
단일 스레드로 작업 처리
특징:
- 순차적 실행
- 한 번에 하나씩
- 느림
"""
print("=== 단일 스레드 ===")
start = time.time()
# 작업 1
print("작업 1 시작")
time.sleep(1) # 1초 걸리는 작업 시뮬레이션
print("작업 1 완료")
# 작업 2
print("작업 2 시작")
time.sleep(1)
print("작업 2 완료")
# 작업 3
print("작업 3 시작")
time.sleep(1)
print("작업 3 완료")
end = time.time()
print(f"총 시간: {end - start:.2f}초\n")
# 예상: 약 3초
# ===== 멀티 스레드 =====
def worker(task_id, duration):
"""
작업을 수행하는 워커 함수
각 스레드가 이 함수를 독립적으로 실행
task_id: 작업 번호
duration: 작업 시간 (초)
"""
print(f"작업 {task_id} 시작")
time.sleep(duration) # 작업 시뮬레이션
print(f"작업 {task_id} 완료")
def multi_thread_example():
"""
멀티 스레드로 작업 처리
특징:
- 병렬 실행
- 동시에 여러 작업
- 빠름
"""
print("=== 멀티 스레드 ===")
start = time.time()
# 스레드 3개 생성
threads = []
for i in range(1, 4): # 작업 1, 2, 3
# threading.Thread: 새 스레드 생성
# target: 스레드가 실행할 함수
# args: 함수에 전달할 인자 (튜플)
thread = threading.Thread(
target=worker,
args=(i, 1) # (task_id, duration)
)
threads.append(thread)
# 스레드 시작
thread.start()
# 주의: start()는 스레드를 시작만 하고 바로 반환
# 스레드는 백그라운드에서 실행됨
# 모든 스레드가 끝날 때까지 대기
for thread in threads:
# join(): 이 스레드가 끝날 때까지 기다림
thread.join()
end = time.time()
print(f"총 시간: {end - start:.2f}초\n")
# 예상: 약 1초 (병렬 실행)
# 실행
single_thread_example() # 약 3초
multi_thread_example() # 약 1초
출력:
=== 단일 스레드 ===
작업 1 시작
작업 1 완료
작업 2 시작
작업 2 완료
작업 3 시작
작업 3 완료
총 시간: 3.00초
=== 멀티 스레드 ===
작업 1 시작
작업 2 시작
작업 3 시작
작업 1 완료
작업 2 완료
작업 3 완료
총 시간: 1.00초
*웹 크롤러(web crawler): 조직적, 자동화된 방법으로 웹(web)을 탐색하는 컴퓨터 프로그램
import threading
import time
import requests # pip install requests
def download_page(url):
"""
웹페이지 다운로드
네트워크 I/O: CPU가 놀고 있음 → 스레드 사용하기 좋은 경우!
url: 다운로드할 URL
"""
try:
print(f"다운로드 시작: {url}")
response = requests.get(url, timeout=5)
print(f"다운로드 완료: {url} ({len(response.text)} bytes)")
except Exception as e:
print(f"오류: {url} - {e}")
def single_thread_crawler(urls):
"""
단일 스레드 크롤러
순차적으로 하나씩 다운로드
"""
print("=== 단일 스레드 크롤러 ===")
start = time.time()
for url in urls:
download_page(url)
print(f"총 시간: {time.time() - start:.2f}초\n")
def multi_thread_crawler(urls):
"""
멀티 스레드 크롤러
동시에 여러 페이지 다운로드
"""
print("=== 멀티 스레드 크롤러 ===")
start = time.time()
threads = []
for url in urls:
thread = threading.Thread(target=download_page, args=(url,))
threads.append(thread)
thread.start()
# 모든 다운로드 완료 대기
for thread in threads:
thread.join()
print(f"총 시간: {time.time() - start:.2f}초\n")
# 사용 예시
urls = [
'https://www.example.com',
'https://www.python.org',
'https://www.github.com',
]
# single_thread_crawler(urls) # 순차: 느림
# multi_thread_crawler(urls) # 병렬: 빠름
스레드들이 같은 메모리를 공유하므로, 동시에 같은 데이터를 수정하면 문제가 발생합니다.
import threading
# 공유 변수
counter = 0
def increment_unsafe():
"""
안전하지 않은 증가
문제:
여러 스레드가 동시에 counter를 수정
→ 경쟁 상태(Race Condition)
→ 예상과 다른 결과
"""
global counter
# 이 코드는 실제로 3단계:
# 1. counter 값 읽기
# 2. 1 증가
# 3. counter에 쓰기
#
# 스레드 A와 B가 동시에 실행하면:
# A: 읽기(0) → 증가(1) → 쓰기(1)
# B: 읽기(0) → 증가(1) → 쓰기(1)
# 결과: 1 (2가 되어야 하는데!)
for _ in range(100000):
counter += 1
def race_condition_demo():
"""
경쟁 상태 시연
"""
global counter
counter = 0
print("=== 경쟁 상태 (Race Condition) ===")
# 스레드 2개 생성
thread1 = threading.Thread(target=increment_unsafe)
thread2 = threading.Thread(target=increment_unsafe)
# 동시 실행
thread1.start()
thread2.start()
# 완료 대기
thread1.join()
thread2.join()
print(f"예상값: 200000")
print(f"실제값: {counter}")
print(f"차이: {200000 - counter}\n")
# 실행할 때마다 결과가 다름!
# 예: 187234, 193451, 198732 등
# 실행
race_condition_demo()
왜 문제가 발생하는가?
counter += 1은 원자적(atomic)이지 않음!
실제로는:
1. LOAD counter (메모리 → 레지스터)
2. ADD 1
3. STORE counter (레지스터 → 메모리)
타이밍 예시:
시간 | 스레드 A | 스레드 B | counter
------|------------------|------------------|--------
t0 | LOAD (0) | | 0
t1 | | LOAD (0) | 0
t2 | ADD 1 → (1) | | 0
t3 | | ADD 1 → (1) | 0
t4 | STORE (1) | | 1
t5 | | STORE (1) | 1
결과: 1 (2가 되어야 하는데!)
import threading
counter = 0
lock = threading.Lock() # 자물쇠 생성
def increment_safe():
"""
안전한 증가
Lock 사용:
- 한 번에 한 스레드만 실행
- 다른 스레드는 대기
- 안전하지만 느림
"""
global counter
for _ in range(100000):
# Lock 획득 (자물쇠 잠그기)
lock.acquire()
try:
# 임계 영역 (Critical Section)
# 한 번에 한 스레드만 실행
counter += 1
finally:
# Lock 해제 (자물쇠 풀기)
# finally: 예외가 발생해도 반드시 실행
lock.release()
def safe_increment_demo():
"""
안전한 증가 시연
"""
global counter
counter = 0
print("=== Lock 사용 (안전) ===")
thread1 = threading.Thread(target=increment_safe)
thread2 = threading.Thread(target=increment_safe)
thread1.start()
thread2.start()
thread1.join()
thread2.join()
print(f"예상값: 200000")
print(f"실제값: {counter}")
print(f"차이: {200000 - counter}\n")
# 항상 200000!
# 더 간단한 방법: with 문
def increment_safe_with():
"""
with 문으로 Lock 사용
장점:
- 자동으로 acquire/release
- 예외 처리 불필요
- 코드 간결
"""
global counter
for _ in range(100000):
with lock: # with 문: 자동으로 lock.acquire()와 lock.release()
counter += 1
# 실행
safe_increment_demo()
def when_to_use_threads():
"""
스레드 사용이 좋은 경우 vs 나쁜 경우
"""
# ===== 좋은 경우: I/O 바운드 =====
"""
I/O 바운드: CPU가 놀고 있는 작업
* I/O Bound: 프로세스가 진행될 때, I/O Wating 시간이 많은 경우로,
파일 쓰기, 디스크 작업, 네트워크 통신을 할 때 주로 나타나며
작업에 의한 병목(다른 시스템과 통신할 때 나타남)에 의해 작업 속도가 결정
예시:
- 파일 읽기/쓰기
- 네트워크 요청
- 데이터베이스 쿼리
- 사용자 입력 대기
이유:
I/O 대기 중에 다른 스레드가 일할 수 있음
"""
# ===== 나쁜 경우: CPU 바운드 =====
"""
CPU 바운드: CPU를 계속 쓰는 작업
* CPU Bound: 프로세스가 진행될 때, CPU 사용 기간이 I/O Wating 보다 많은 경우로,
주로 행렬 곱이나 고속 연산을 할 때 나타나며 CPU 성능에 의해 작업 속도가 결정
예시:
- 복잡한 계산
- 이미지 처리
- 비디오 인코딩
- 암호화/복호화
이유:
Python은 GIL(Global Interpreter Lock)로 인해 한 번에 한 스레드만 Python 코드 실행
→ CPU 바운드에서는 멀티스레딩 효과 없음
→ 대신 multiprocessing 사용!
"""
import os
import threading
def how_many_threads():
"""
적절한 스레드 수
"""
# CPU 코어 수 확인
cpu_count = os.cpu_count()
print(f"CPU 코어: {cpu_count}개")
# 현재 활성 스레드 수
active = threading.active_count()
print(f"활성 스레드: {active}개")
"""
가이드라인:
I/O 바운드:
- 스레드 수 = CPU 코어 수 × 2 ~ 10
- 대부분 대기하므로 많이 만들어도 OK
CPU 바운드:
- 스레드 수 = CPU 코어 수
- 더 많이 만들면 오버헤드만 증가
실제:
- 테스트해보고 결정
- 너무 많으면 컨텍스트 스위칭 비용
- 너무 적으면 자원 낭비
"""
how_many_threads()
import threading
def thread_debugging_tips():
"""
스레드 디버깅
"""
# 스레드 이름 지정
def worker():
name = threading.current_thread().name
print(f"[{name}] 작업 중...")
thread = threading.Thread(
target=worker,
name="Worker-1" # 이름 지정
)
thread.start()
thread.join()
# 데몬 스레드
"""
데몬 스레드:
- 백그라운드 작업용
- 메인 스레드 종료 시 자동 종료
- 로그, 모니터링 등에 사용
"""
def daemon_worker():
while True:
print("백그라운드 작업...")
threading.Event().wait(1)
daemon = threading.Thread(
target=daemon_worker,
daemon=True # 데몬으로 설정
)
daemon.start()
# 메인 종료하면 daemon도 자동 종료
thread_debugging_tips()
스레드
정의:
프로세스 내부의 실행 흐름
특징:
- 메모리 공유
- 가볍고 빠름
- 통신 쉬움
- 동기화 필요
프로세스 vs 스레드
프로세스:
독립적, 무거움, 안전, 통신 어려움
스레드:
공유, 가벼움, 위험, 통신 쉬움
선택:
독립성/안정성 → 프로세스
속도/효율 → 스레드
멀티스레딩
장점:
- I/O 대기 중 다른 작업 가능
- 반응성 향상 (UI가 멈추지 않음)
- 자원 효율
단점:
- 경쟁 상태 (Race Condition)
- 디버깅 어려움
- 데드락 가능
*교착 상태(Deadlock): 운영체제에서 2개 이상의 프로세스가 서로 상대방이 가진 자원을 기다리며
무한히 대기하여, 결과적으로 어떤 작업도 진행되지 못하고 멈추는 현상
스레드 안전성
문제:
여러 스레드가 같은 데이터 동시 수정
→ 예상치 못한 결과
해결:
Lock 사용
→ 한 번에 한 스레드만 실행
→ 안전하지만 느려짐
실무 가이드
I/O 바운드: 스레드 ✓
CPU 바운드: 프로세스 (multiprocessing)
스레드 수:
I/O: CPU 코어 × 2~10
CPU: CPU 코어 수
항상 테스트!
[09-03] 컨텍스트 스위칭 (Context Switching)
이전 글: [09-01] 프로세스
다음 글: [09-03] 컨텍스트 스위칭
시리즈: P1. Computer Science