-
Python GILTIL 2024. 1. 3. 00:59728x90
파이썬의 표준 구현체로 C언어로 작성된 Cpython이 있다. Cpython은 두 단계를 거쳐서 파이썬 프로그램을 실행한다.
- 소스 코드 구문 분석과 바이트 코드 변환: 파이썬 소스 코드는 먼저 구문 분석 과정을 거쳐 추상 구문 트리(Abstract Syntax Tree, AST)로 변환된다. 그 후, 이 AST는 파이썬의 중간 표현인 '바이트 코드'로 변환된다. 바이트 코드는 기계어가 아닌, 파이썬 가상 머신이 이해할 수 있는 형태의 코드다.
- 스택 기반 인터프리터 실행: 변환된 바이트 코드는 스택 기반의 인터프리터를 통해 실행된다. 스택 기반 인터프리터는 명령어를 수행하기 위해 데이터를 스택에 저장하고, 이를 순차적으로 처리한다.
바이트 코드 인터프리터에는 파이썬 프로그램이 실행되는 동안 일관성 있게 유지해야 하는 상태가 존재한다. 이는 변수 값, 함수 호출 스택, 메모리 할당 등 프로그램의 실행 상태를 포함한다.
CPython은 전역 인터프리터 락(GIL)을 사용해 일관성을 강제로 유지한다.
GIL이란?
GIL은 Cpython에서 멀티스레딩 환경에서 데이터 무결성을 보장하기 위해 사용되는 뮤텍스다. GIL은 한 순간에 하나의 스레드만이 파이썬 객체와 상호작용할 수 있게 제한한다.
GIL은 멀티스레딩에서 발생할 수 있는 동시성 문제, 예를 들어 레이스 컨디션(Race Condition)이나 데이터의 불일치 등을 방지한다.
GIL이 풀려고 하는 문제는?
멀티 스레드 프로그래밍에서 스래드들은 하나의 프로세스에서 실행되기 때문에 전역변수나 heap 메모리 자원같은 공유 자원에 여러 스레드가 접근하고 변경할 수 있다. 이를 해결하기 위한 방법이 동기화이고, 뮤텍스와 세마포어와 같은 lock을 통해 해결하는 방식이 그 중 하나이다.
적절한 조건에 따라 lock을 점유하거나 해제하지 않으면, 경쟁 상태가 발생하거나 성능저하가 발생할 수 있다. 이로 인해 멀티 스레딩 프로그래밍은 많은 어려움을 야기한다.
파이썬은 GIL을 통해 이 문제를 단순하게 해결한다. Thread가 자기 차례가 될 때, 파이썬 인터프리터 자체에 lock을 건다. 그리고 자기 차례가 끝나면 인터프리터 자체에 걸려있단 lock을 해제한다. 그리고 하나의 프로세스에는 하나의 파이썬 인터프리터만 존재할 수 있다.
이를 통해 하나의 스레드만 실행되는 것을 보장하기 때문에 경쟁 상태 문제가 사라진다.

GIL의 문제
애초에 멀티 스레드를 사용하는 이유는 동시에 여러 스레드를 실행시켜서 최종적으로 성능을 개선하는 것이다. 하지만 GIL은 인터프리터 자체에 lock을 걸었다 풀기 때문에 스레드를 하나씩 실행하기 때문에, 실행 시간은 싱글 스레드와 큰 차이가 나지 않게 된다. 파이썬이 다른 언어보다 속도가 느린 이유다.
그런데 왜 파이썬은 GIL은 선택했는가
파이썬의 초기를 살펴봐야 한다. 당시 프로그래밍에는 멀티 스레드 개념 자체가 없었고 모든 프로그램은 싱글 스레드 프로그램이었다. 멀티 스레드가 대세로 등장하면서 파이썬도 멀티 스레드를 지원하여야 했는데, 당시 파이썬에는 많은 C 기반의 extension이 있었고, 이들은 thread-safe(Multi-thread 환경에서도 이 코드가 문제없이 작동)하지 않았다.
모든 thread-safe하지 않은 이 extension을 재구현하는 것은 현실적으로 불가능하기 때문에, 결국 멀티 스레드 환경에서는 사용할 수 없다.
이에 대한 대안이 바로 GIL이다. GIL의 도입을 통해 파이썬 인터프리터 단계에서 Thread-safety를 보장하므로 기존 extension을 사용할 수 있다.
파이썬의 장점 중 하나가 폭넓은 라이브러리 생태계이기 때문에 GIL은 파이썬에 멀티 스레드를 제공할 수 있는 효율적 방법이었다.
이런 한계에도 파이썬이 스레드를 지원하는 이유는 무엇일까?
1. 멀티 스레드를 사용하면 프로그램이 동시에 여러 일을 하는 것처럼 보이게 만들기 쉽다.
동시성 작업의 동작을 잘 조화시키는 코드를 직접 작성하는 것은 힘들다. Thread를 사용하면 작성한 함수를 파이썬으로 동시에 실행시킬 수 있다. GIL로 인해 스레드 중 하나만 진행할 수 있지만 Cpython이 어느정도 균일하게 각 스레드를 실행시켜주기 때문에, 멀티 스레드를 통해 여러 함수를 동시에 실행할 수 있다.
2. blocking I/O를 다루기 위해
블로킹 IO는 파이썬이 특정 시스템 콜을 사용할 때 일어난다. 파이썬 프로그램은 시스템 콜을 사용해 운영체제가 자기 대신 외부 환경과 상호작용 하도록 한다.
파일 입출력, 네트워크 등 작업이 블로킹 IO에 속한다. 스레드를 사용하면 운영체제가 시스템 콜 요청에 응답하는 데 걸리는 시간 동안 파이썬 프로그램이 다른 일을 할 수 있다.
병렬화한 버전은 순차적으로 실행한 경우보다 시간이 줄어든다. 이는 GIL로 인해 생기는 한계가 있더라도 여러 스레드를 통해 파이썬을 병렬로 실행할 수 있음을 보여준다. GIL은 파이썬 프로그램이 병렬로 실행되지 못하게 막지만, 시스템 콜에 영향을 끼칠 수 없기 때문이다.
파이썬 스레드가 시스템 콜을 하기 전에 GIL을 해제하고, 시스템 콜에서 반환되자마자 GIL을 다시 획득하기 때문이다.
스레드 외에도 asyncio 내장 모듈 등 블로킹 IO를 처리하는 방법이 있고 장단점이 존재한다. 이런 대안들을 사용할 때 고려할 점은 각각의 실행 모드에 맞게 코드를 변경하는 추가 작업이 필요하다.
그러면 파이썬에서 멀티 스레드는 느린건가?
꼭 그렇지 않다. 앞서 블로킹 IO 작업을 실행하는 동안 시스템 콜 요청에 따라 GIL을 해제하고 반환되자마자 GIL을 획득하기 때문에 그 사이에 다른 스레드가 cpu 동작을 둥시에 실행할 수 있다. 따라서 cpu 동작이 많지 않고 IO 동작이 많은 프로그램에서는 멀티 스레드의 이점을 얻을 수 있다.
GIL이 있으니까 thread safe한가?
GIL은 Python의 표준 라이브러리 내부에서의 작업에 대해서는 thread-safe하게 작동한다. 예를 들어, 내장 데이터 구조(리스트, 딕셔너리 등)의 기본 연산은 GIL로 인해 thread-safe하다.
그러나 Python 코드나, 특히 Python 외부의 시스템 레벨 리소스(파일 시스템, 네트워크 소켓 등)를 사용할 때 thread safety는 Python 개발자가 직접 관리해야 한다. 스레드가 GIL을 해제하므로, 여기서는 GIL이 자동으로 thread safety를 보장하지 않는다.
스레드 안전성 확보 방법
완전한 thread safety를 달성하기 위해서는 추가적인 동기화 메커니즘(예: 락, 세마포어)을 사용해야 한다.
멀티스레딩 환경에서 공유 자원에 접근할 때는 적절한 락(lock)을 사용하여 동시 접근을 관리해야 한다.
이제 GIL을 없애도 되지 않나?
GIL을 제거했지만 싱글 스레드 프로그램의 성능이 대폭 떨어지는 문제가 발생했다. 대부분의 파이썬이 싱글 스레드임을 감안하면 이런 문제가 해결되기 전까지 GIL을 제거하기는 어려워 보인다.
파이썬에서 GIL 삭제된다⋯“병렬 처리의 혁신적 진전”
하지만 파이썬 운영 위원회(Python Steering Council)가 “C파이썬에서 전역 인터프리터 잠금(Global Interpreter Lock)을 선택 사항으로 두자”는 PEF 703 제안을 승인하는 쪽으로 가닥을 잡았다
파이썬 핵심 개발자들은 싱글 스레드 프로그램의 속도를 저하시키지 않으면서 제거할 수 있어야 한다는 전제 조건을 두고 C파이썬에서 GIL을 제거하기로 했다. 기본 빌드로 채택되는 단계에 이르기까지 길면 5년 정도 걸릴 것으로 예상된다고 한다.
728x90'TIL' 카테고리의 다른 글
스레드를 많이 사용할 수록 성능이 증가할까? (0) 2024.01.02 멀티 프로세스 vs 멀티 스레드 (2) 2024.01.02 Thread & Process - 2 (1) 2023.12.28 Docker로 개발환경을 구축하는 이유 (0) 2023.12.27 Thread & Process - 1 (1) 2023.12.27