Chapter 5

동시성과 병렬성

  • 5.1 subprocess로 자식 프로세스 관리
  • 5.2 스레드는 블로킹 I/O 용
  • 5.3 Lock으로 데이터 경합 방지
  • 5.4 Queue로 스레드 간 작업 조율
  • 5.5 동시성이 필요한 시점 인식
  • 5.6 새 Thread 인스턴스 생성 지양
  • 5.7 Queue 기반 동시성의 리팩토링 한계
  • 5.8 ThreadPoolExecutor
  • 5.9 코루틴으로 고성능 I/O
  • 5.10 스레드에서 asyncio로 포팅
  • 5.11 스레드와 코루틴 혼합
  • 5.12 asyncio 이벤트 루프 응답성
  • 5.13 concurrent.futures로 진정한 병렬성

파이썬 동시성의 핵심은 "뭘 써야 하는지"를 아는 거야. 도구는 많은데, 잘못 고르면 오히려 느려지거든.

출발점은 GIL이야. 파이썬의 Global Interpreter Lock 때문에 스레드는 CPU 바운드 작업을 병렬로 실행하지 못해. 스레드로 CPU 작업을 병렬화하면 GIL 경합 때문에 단일 스레드보다 느려질 수도 있지. 하지만 블로킹 I/O(파일, 네트워크, 시스템 호출)에서는 GIL이 해제되니까 스레드가 유효해. 그러니까 스레드는 I/O 전용이야. CPU 병렬성이 필요하면 **ProcessPoolExecutor**를 써서 GIL을 완전히 우회해야 진짜 멀티코어 병렬성을 얻을 수 있어.

GIL이 있으니까 Lock이 필요 없다고 생각하면 틀렸어. += 같은 연산이 여러 바이트코드로 분해되니까 그 사이에 컨텍스트 스위칭이 일어날 수 있거든. 공유 상태를 수정하는 코드에는 항상 Lock이 필요해. 작업마다 새 Thread를 만드는 것도 위험한데, 수천 개가 되면 OS 리소스를 잡아먹으니까 **ThreadPoolExecutor**로 스레드 풀을 관리하는 게 맞지.

대량 동시 I/O가 필요하면 스레드보다 asyncio 코루틴이 답이야. 하나의 스레드에서 협력적으로 실행되니까 Lock도 필요 없고, 수만 개의 동시 연결도 가능해. 다만 이벤트 루프에서 CPU 바운드 작업을 실행하면 다른 코루틴이 블로킹되니까, 무거운 작업은 run_in_executor()로 별도 스레드나 프로세스에서 돌려야 하고. 기존 스레드 코드를 asyncio로 포팅할 때는 점진적 마이그레이션이 핵심이야 — 한 번에 전부 바꾸는 건 비현실적이고, 가장 안쪽 I/O부터 async/await로 바꿔나가는 거지. 그리고 무엇보다, 동시성은 프로파일링으로 병목을 확인한 후에 도입하자. 섣부른 동시성은 섣부른 최적화와 같아.


정리

5장 읽고 기억할 거 세 가지:

  1. 스레드는 블로킹 I/O에만 쓰고, CPU 병렬성은 ProcessPoolExecutor를 쓰자. GIL 때문에 스레드로 CPU 작업을 병렬화할 수 없다.
  2. 대량 동시 I/O에는 asyncio가 스레드보다 효율적이다. 수만 개의 동시 연결이 필요하면 코루틴이 답이고, 기존 코드는 점진적으로 포팅하자.
  3. GIL은 Lock을 대체하지 않고, 동시성은 측정 후에 도입하자. 공유 상태에는 항상 Lock이 필수다.