Python의 멀티스레딩은 Global Interpreter Lock(GIL)로 인해 제약을 받으며, 이는 Python 인터프리터가 멀티스레딩을 처리하는 방식과 운영 체제가 스레드를 스케줄링하는 방식에 영향을 미칩니다. asyncio와의 비교를 통해 Python이 제공하는 동시성 모델의 차이를 이해할 수 있습니다. 이 글에서는 Python 인터프리터, OS 스레드 스케줄러, GIL, 멀티스레딩, 그리고 asyncio 간의 관계를 탐구합니다.
1. Python 인터프리터와 OS 스레드 스케줄러
1.1 Python 인터프리터
- Python 인터프리터는 Python 코드를 바이트코드로 컴파일한 후 실행하는 핵심 엔진입니다.
- CPython 인터프리터는 Global Interpreter Lock(GIL)을 사용하여 한 번에 하나의 스레드만 Python 바이트코드를 실행하도록 제한합니다.
- 운영 체제 관점에서 Python 인터프리터는 OS가 관리하는 하나의 프로세스로 실행됩니다.
1.2 OS 스레드 스케줄러
- Python의
threading 모듈은 운영 체제 수준의 스레드를 생성하며, OS는 이 스레드들을 독립적으로 관리합니다.
- OS 스레드 스케줄러는 실행 가능한 모든 스레드에 CPU 시간을 할당합니다.
- 그러나 GIL로 인해 OS가 여러 스레드에 CPU 시간을 할당하더라도 한 번에 하나의 Python 스레드만 Python 바이트코드를 실행할 수 있습니다.
2. 멀티스레딩의 동작과 GIL의 역할
2.1 GIL(Global Interpreter Lock)의 역할
- GIL은 Python 인터프리터 내에서 동작하며, 한 번에 하나의 스레드만 Python 바이트코드를 실행하도록 제한합니다.
- 멀티스레딩 환경에서 Python 객체에 대한 접근을 보호하고 참조 카운트를 관리하여 스레드 안전성을 보장합니다.
- GIL은 특정 조건에서 다른 스레드가 획득할 수 있도록 허용합니다:
- 현재 스레드가 GIL을 해제할 때(예: C 확장 모듈에서 블로킹 I/O 작업 중).
- 일정한 바이트코드 명령 수를 실행한 후(Python은 주기적으로 GIL 해제를 체크합니다).
2.2 스레드 생성 및 실행 흐름
- 스레드 생성:
- Python의
threading 모듈을 사용하여 OS 수준의 스레드를 생성합니다.
- 스레드 스케줄링:
- 각 스레드는 OS 스레드 스케줄러에 의해 스케줄링됩니다.
- GIL의 실행 제어:
- GIL은 한 번에 하나의 스레드만 Python 코드를 실행하도록 제한합니다.
- 스레드가 GIL을 해제하는 블로킹 I/O 작업을 수행할 때, 다른 스레드가 GIL을 획득하여 실행할 수 있습니다.
2.3 GIL과 대기 상태
- 운영 체제 관점:
- GIL을 소유하지 않은 스레드도 여전히 "실행 가능" 상태로 간주되며, OS는 이들에게 CPU 시간을 할당할 수 있습니다.
- Python 인터프리터 관점:
- GIL이 없는 스레드는 Python 바이트코드를 실행할 수 없어 "논리적 대기 상태"에 있습니다.
- 비효율성:
- GIL로 인해 진행할 수 없는 스레드에 CPU 시간이 할당되면 CPU 자원 활용이 비효율적이 됩니다.
3. 멀티스레딩의 비효율과 컨텍스트 스위칭
3.1 CPU 바운드 작업에서의 비효율성
- CPU 바운드 작업(예: 수학 계산)에서는 GIL이 병목 현상이 되어 여러 스레드가 Python 바이트코드를 병렬로 실행하지 못합니다.
- 스레드들은 GIL을 획득하기 위해 빈번하게 경쟁하며, 이는 오버헤드만 증가시키고 성능 향상은 없습니다.
- 결과적으로 멀티스레딩은 CPU 집약적인 작업에서 기대한 속도 향상을 제공하지 못합니다.
3.2 컨텍스트 스위칭 오버헤드
- 불필요한 OS 수준의 컨텍스트 스위칭:
- OS 스케줄러는 GIL을 알지 못하므로 진행할 수 없는 스레드 사이에서도 스위칭할 수 있습니다.
- 이는 CPU 사이클의 낭비와 불필요한 컨텍스트 스위칭을 초래합니다.
- 컨텍스트 스위칭 비용:
- 각 컨텍스트 스위칭 시 스레드 상태 저장 및 복구로 인한 오버헤드가 발생합니다.
- GIL 경쟁으로 인한 빈번한 스위칭은 이러한 오버헤드를 증가시킵니다.
3.3 GIL 소유권 전환과 OS 컨텍스트 스위칭의 구분
- GIL 소유권 전환:
- GIL은 인터프리터 수준에서의 실행 권한을 제어하며, 스레드 간의 GIL 소유권을 전환합니다.
- 이는 OS 수준의 컨텍스트 스위칭이 아닌, GIL의 소유권 변경입니다.
- OS 스레드 스케줄러의 컨텍스트 스위칭:
- OS는 물리적인 실행을 관리하며, 스레드 사이에서 CPU 시간을 할당합니다.
- 중요한 구분점:
- 스레드가 OS에 의해 스케줄링되더라도 GIL 없이는 Python 코드를 실행할 수 없습니다.
- OS 스케줄링과 GIL 소유권 간의 불일치는 비효율을 유발할 수 있습니다.
4. 멀티스레딩과 asyncio의 비교
4.1 멀티스레딩의 한계
- I/O 바운드 작업:
- 특정 블로킹 I/O 작업 중에는 GIL이 해제되어 다른 스레드가 실행될 수 있습니다.
- 그러나 스레드 관리와 컨텍스트 스위칭 오버헤드는 여전히 존재합니다.
- CPU 바운드 작업:
- GIL로 인해 Python 바이트코드의 병렬 실행이 불가능합니다.
- 스레드 수를 늘려도 성능 향상이 없으며 오버헤드만 증가합니다.
4.2 asyncio의 동작 원리
asyncio는 단일 스레드의 이벤트 루프를 사용하여 비동기 작업(코루틴)을 관리하고 실행합니다.
- 이벤트 루프는 코루틴을 스케줄링하며,
await 키워드를 통해 논블로킹 I/O 작업 시 다른 코루틴으로 전환합니다.
- GIL 경쟁 없음:
- 모든 코드가 하나의 스레드에서 실행되므로 GIL 경쟁이 없습니다.
- 효율적인 컨텍스트 스위칭:
- 코루틴 간 전환은 가볍고 인터프리터 내에서 관리됩니다.
- 낮은 메모리 오버헤드:
- 코루틴은 스레드보다 메모리 효율적이며, 동일한 스레드 스택을 공유합니다.
- 높은 동시성:
- I/O 바운드 애플리케이션에 적합하며, 많은 동시 연결을 효율적으로 처리합니다.
4.3 주요 차이점
| 특성 | 멀티스레딩(GIL 기반) | asyncio |
|---|
| 실행 모델 | OS 수준의 스레드 활용 | 단일 스레드의 이벤트 루프 |
| Python 바이트코드 실행 | GIL로 인해 한 번에 하나의 스레드만 실행 가능 | 이벤트 루프가 코루틴을 비동기적으로 관리 |
| I/O 작업 처리 | 특정 블로킹 I/O 중에 GIL 해제 가능 | 논블로킹, 비동기 I/O |
| CPU 사용 효율성 | GIL 경쟁으로 인한 컨텍스트 스위칭 오버헤드 발생 | 이벤트 루프 내에서 효율적인 컨텍스트 전환 |
| 메모리 사용량 | 스레드 스택과 컨텍스트로 인해 높음 | 코루틴은 경량으로 메모리 사용량이 낮음 |
| 복잡성 | 스레드 동기화 및 관리 필요 | async/await 문법 이해 필요 |
멀티스레딩이 GIL로 인해 가지는 한계를 이해하고, asyncio가 I/O 바운드 작업에서 어떻게 효율적인지를 알면 개발자는 Python 애플리케이션에 적합한 동시성 모델을 선택할 수 있습니다.