CPython

Cpython 병렬성과 동시성 /(2) 멀티 스레딩

25G 2023. 2. 12. 21:55

멀티 스레딩

CPython은 스레드를 생성, 스폰, 제어 할 수 있는 파이썬용 고수준 API와 저수준 API를 제공한다.

  1. pthread: POSIX 스레드 ( 리눅스, macos)
  2. nt threads: NT 스레드 ( 윈도우 )

프로세스는 다음과 같은 요소들을 같는다.

  • 서브루틴의 스택
  • 메모리와 힙
  • 운영체제의 파일, 잠금, 소켓에 접근할 수 있는 권한

단일 프로세스의 가장 큰 제약은 운영체제가 실행 파일마다 하나의 프로그램 카운터를 가진다는 것이다.
이 문제를 해겨라기 위해 최신 운영 체제는 실행을 여러 스레드로 분기할 수 있도록 운영체제에 신호를 보낼 수 있다.
각 스레드는 저마다 다른 프로그램 카운터를 가지지만 호스트 프로세스와 리소스를 공유한다. 또한 각 스레드는 별도의 콜 스택을 가지고 있기 때문에 다른 함수를 실행할 수도 있다.

GIL

CPython에서 스레드는 C API기반으로 하지만 결국 파이썬 스레드다. 즉, 모든 파이썬 스레드는 평가 루프를 통해 파이썬 바이트 코드를 실행해야하낟.
파이썬 평가 루프 스레드는 안전하지 못하다. 가비지 컬렉터를 비롯해 인터프리터 상태를 구성하는 많은 부분은 전역적이고 공유 상태다. 이런 구조 때문에 GIL(global interpreter lock) 전역 인터프리터 잠금이라는 거대한 잠금을 구현했다. 프레임 평가 루프에서 명령코드를 실행하기 전에 스레드는 GIL을 얻고, 명령 코드를 실행한 후에 GIL을 해제한다.
GIL은 전역 스레드 안정성을 제공하지만 긴시간이 걸리는 명령이 실행중이면 다른 스레드들은 그 시간동안 GIL이 해제되기만을 기다려야한다.
특정한 프레임 실행 작업이 GIL을 영원히 보유하는 걸 막기 위해 평가루프상태에는 gil_drop_request 라는 플래그가 있다. 프레임의 각 바이트코드 연산이 완료될 때마다 플래그가 설정되고 GIL이 일시적으로 해제된다.

파이선 스레드

threading.Thread()를 사용해 각 포트에 대해 스래드를 스폰하도록 하고 threading.Thread()는 multiprocessing과 비슷한 api를 제공하는데 멀티 스레딩 api는 target과 args두가지 인자를 받는다. 실행할 콜러블은 target으로 전달되고 실행할 콜러블의 인자는 튜플 형태로 args로 전달된다.

멀티 스레딩의 오버헤드는 멀티프로세싱에서 새 프로세스 시작시 오버헤드보다 훨씬 작아서 멀티프로세싱을 사용한 구현보다 50%에서 60% 가량 빠르게 실행된다.

스레드 상테

CPython은 독자적인 스레드 관리 구현을 제공한다. 스레드가 평가 루프에서 파이썬 바이트코드를 실행해야 하기 때문에 Cpython에서 스레드를 실행하는 것은 운영체제 스레드를 스폰하는 것 만큼 간단하지 않다.
PyThread는 코드 객체를 실행하며 인터프리터에 의해 스폰된다.

  • CPython은 런타임이 하나 있고 이 런타입은 런타임 상태를 가지고 있다.
  • CPython은 하나이상의 인터프리터를 가질 수 있다.
  • 인터프리터는 인터프리터 상태를 가진다.
  • 인터프리터는 코드 객체를 일련의 프레임 객체로 변환한다.
  • 인터프리터는 스레드를 최소 하나 가진다. 이때 각 스레드는 스레드 상태를 가진다.
  • 프레임 객체는 프레임 스택위에서 실행된다.
  • CPython은 값 스택에서 변수를 참조한다.
  • 인터프리터 상태는 스레드들을 연결 리스트로 가지고 있다.

스레드 상태 타입 PyThreadState는 다음 프로퍼티들을 포함해 서른개가 넘는 프로퍼티가 있다.

  • 고유식별자
  • 다른 스레드 상태와 연결된 연결리스트
  • 스레드를 스폰한 인터프리터의 상태
  • 현재 실행 중인 프레임
  • 현재 재귀 깊이
  • 선택적 추적함수들
  • 현재 처리중인 예외
  • 현재 처리중인 비동기 예외
  • 여러 예외가 발생할때의 예외 스택
  • GIL카운터
  • 비동기 제너레이터 카운터
    멀티프로세싱의 준비 데이터 처럼 스레드도 부트 상태가 필요하지만 스레드는 부모와 메모리 공간을 공유하기 때문에 데이터를 직렬화 해서 파일 스트림으로 전송할 필요는 없다.

새 스레드는 다음과 같은 순서로 인스턴스화 한다.

  1. bootstate를 생성한 후 args와 kwargs인자와 함께 target에 연결된다.
  2. bootstate를 인터프리터 상태에 연결한다.
  3. 새 PyThreadState를 생성하고 현재 인터프리터에 연결한다.
  4. PyEval_InitThreads()를 호출해서 GIL이 홠어화되지 않았을 경우 GIL을 활성화한다.
  5. 운영체제에 맞는 PyThread_start_new_thread 구현을 사용해서 새 스레드를 시작한다.

부트스트랩 함수는 스레드를 초기화 하고 PyObject_Call()을 사용해 target을 호출한다. 스레드는 콜러블을 실행한 후 종료된다.

POSIX 스레드

POSIX 스레드 구현은 Python/thread_pthread.h에서 찾을수있다. 이 구현은 C API를 추상화하고 몇가지 추가 안정장치와 최적화를 제공한다.

스레드의 스택 크기는 설정 가능한 값이다. 파이썬은 자체적인 스택 프레임을 가지고 있다. 만약 재귀 루프가 발생해서 프레임 실행이 재귀 깊이 재한에 도달하면 파이썬은 RecursionError 예외를 발생시킨다.
하지만 pthreads도 자체적인 스택크기를 가지고 있기 때문에 파이썬의 깊이제한과 충돌이 발생할 수도 있다 스레드 스택 크기가 파이썬의 최대 프래임 깊이보다 작다면 RecursionError가 발생하기 전에 파이썬 프로세스 전체가 충돌할 것이다.최신 POSIX 호환 운영 체제들은 pthreads용 시스템 스케줄링을 지원한다. 스레드 프로퍼티들을 구성했으면 pthread_create() api로 스레드를 생성한다. 이때 이 API가 새 스레드에서 부트스트랩 함수를 실행한다.
마지막으로 스레드 아이디로 사용하기 위해 스레드 핸들 phread_t 를 unsigned long으로 변환후 반환한다.

요약

파이썬 스레드의 구현은 광법위하며 스레드 간 데이터 공유, 객체 잠금과 자원에 대한 다양한 메커니즘을 제공한다. 스레드은 I/O집약적인 파이썬 애플리케이션의 런타임 성능을 높일 수 있는 훌륭하고 효율적인 방법이다.