본문 바로가기
C.W.K.
Stream
Lesson 03 of 05 · published

스레드와 GIL — I/O에는 도움, CPU에는 한계

~18 min · threading, gil, lock, thread

Level 0호기심
0 XP0/93 lessons0/23 achievements
0/100 XP to next level100 XP to go0% complete

일반 CPython의 GIL은 한 순간에 한 스레드만 Python 바이트코드를 실행하게 해 CPU 작업을 병렬화하지 못하지만, I/O 대기 중에는 풀리므로 동기 네트워크·파일 라이브러리를 함께 기다리는 데 쓸 수 있어. 3.13의 자유 스레딩 빌드는 선택적 실험 경로야.

Thread·start·join 아래에 Lock, RLock, Event, Condition과 스레드 안전 Queue가 있고, 보통은 ThreadPoolExecutor가 작업과 결과를 더 단순하게 관리해. GIL도 검사 후 갱신 같은 여러 단계의 경쟁 조건을 막지 않으니 공유 상태는 잠가.

Code

스레드와 GIL의 CPU 한계·python
import threading
import time

def worker(name, delay):
    print(f"{name}: 시작")
    time.sleep(delay)                     # I/O 스타일 — GIL release
    print(f"{name}: 완료")

start = time.perf_counter()
threads = [
    threading.Thread(target=worker, args=(f"t{i}", 1))
    for i in range(3)
]
for t in threads:
    t.start()
for t in threads:
    t.join()
print(f"경과: {time.perf_counter() - start:.2f}s")
# 경과: ~1s (병렬 I/O — sleep 동안 GIL release)

# 이제 CPU 바운드 — GIL 직렬화
def cpu_work(n):
    total = 0
    for _ in range(n):
        total += 1
    return total

start = time.perf_counter()
threads = [threading.Thread(target=cpu_work, args=(10_000_000,)) for _ in range(3)]
for t in threads: t.start()
for t in threads: t.join()
print(f"cpu 경과: {time.perf_counter() - start:.2f}s")
# 단일 스레드 시간의 ~3 배 — GIL 이 병렬성 막음
Lock으로 공유 상태 지키기·python
import threading

counter = 0
lock = threading.Lock()

def increment(times):
    global counter
    for _ in range(times):
        with lock:                        # atomic
            counter += 1

threads = [threading.Thread(target=increment, args=(100_000,)) for _ in range(4)]
for t in threads: t.start()
for t in threads: t.join()
print(counter)                            # 400000 — lock 없으면 더 적게
Queue로 스레드 사이에 값 보내기·python
import threading
import queue

q = queue.Queue(maxsize=10)

def producer():
    for i in range(5):
        q.put(f"item-{i}")
    q.put(None)                           # 완료 신호

def consumer():
    while True:
        item = q.get()
        if item is None:
            break
        print("got", item)
        q.task_done()

t_p = threading.Thread(target=producer)
t_c = threading.Thread(target=consumer)
t_p.start(); t_c.start()
t_p.join(); t_c.join()
ThreadPoolExecutor로 작업 관리하기·python
from concurrent.futures import ThreadPoolExecutor
import time

def fetch(url):
    time.sleep(0.5)                       # I/O 시뮬레이트
    return f"got {url}"

urls = ["a", "b", "c", "d", "e"]

start = time.perf_counter()
with ThreadPoolExecutor(max_workers=5) as pool:
    results = list(pool.map(fetch, urls))
print(results)
print(f"{time.perf_counter() - start:.2f}s")
# 5 다 병렬 실행 (I/O 바운드) — 총 ~0.5s

External links

Exercise

ThreadPoolExecutor(max_workers=4)로 1초 걸리는 가짜 URL 여덟 개를 실행하고 as_completed 순서로 출력해. 총 시간이 약 2초인지 확인해.

Progress

Progress is local-only — sign in to sync across devices.
이 페이지에서 버그를 발견하셨거나 피드백이 있으세요?문제 신고

댓글 0

🔔 답글 알림 (로그인 필요)
로그인댓글을 남기려면 로그인해 주세요.

아직 댓글이 없어요. 첫 댓글을 남겨보세요.