Python 인터프리터, OS 스레드 스케줄러, Python 스레드, 그리고 asyncio의 관계

Gyutae Ha·2024년 11월 26일
post-thumbnail

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 스레드 생성 및 실행 흐름

  1. 스레드 생성:
    • Python의 threading 모듈을 사용하여 OS 수준의 스레드를 생성합니다.
  2. 스레드 스케줄링:
    • 각 스레드는 OS 스레드 스케줄러에 의해 스케줄링됩니다.
  3. 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 애플리케이션에 적합한 동시성 모델을 선택할 수 있습니다.

profile
Search Engineer

0개의 댓글