C.W.K.
Stream
Lesson 03 of 05 · published

Threading 과 GIL

~18 min · threading, gil, lock, thread

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

GIL — 뭐고 왜 중요

CPython 에 Global Interpreter Lock — 한 번에 한 스레드만 Python 바이트코드 실행 허용하는 mutex. CPU 바운드 두 스레드가 Python 병렬 실행 X — 차례로 함. GIL 이 I/O 동안 release 되니 threading 이 I/O 무거운 작업엔 여전히 유용. CPU 무거운 작업엔 multiprocessing (다음 lesson) 또는 3.13+ 의 새 free-threaded 모드 (실험).

threading 사용 시기

(1) 동시 I/O 필요한데 third-party 라이브러리가 async 지원 X (sync DB 드라이버, sync HTTP 클라이언트). (2) blocking 호출 많고 asyncio 리팩터 viable X. (3) 스레드에서 실행해야 하는 코드와 통합 (일부 GUI 프레임워크, 일부 C extension). 현대 async-aware 코드엔 threading 이 첫 선택 거의 X.

threading API — 기본

threading.Thread(target=fn, args=(...)) 가 스레드 생성. .start() 가 실행. .join() 이 기다림. threading.Lock(), threading.RLock(), threading.Event(), threading.Condition() 이 동기화 primitive. queue.Queue 가 thread-safe + 스레드 간 데이터 전달 맞는 방법.

concurrent.futures — 더 고레벨 인터페이스

대부분 threading 작업엔 concurrent.futures.ThreadPoolExecutor 가 맞는 도구. task submit, Future 객체 받기, 결과 수집. 스레드 직접 관리보다 깔끔. ProcessPoolExecutor 가 프로세스에 같은 방식 — 같은 API, 다른 동시성 모델.

Code

기본 threading — GIL 제약·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 — 공유 state 보호·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.Queue — thread-safe pipe·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 — raw 스레드보다 쉬움·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

ThreadPoolExecutortime.sleep 시뮬레이트한 8 URL list 병렬 fetch, max_workers=4. pool.submit 사용 + futures 수집, as_completed 로 끝나는 대로 각 결과 출력. 총 시간 ~2 초 (8 URL, 4 동시, 각 1 초) 확인, 8 초 X.

Progress

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

댓글 0

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

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