[Python 3.13] Python에서 GIL을 비활성화 할 수 있다?
Python 3.13과 GIL
Medium을 구독하면서 읽게된 글을 통해 Python 3.13 버전이 출시되었다는 것을 알게 되었는데, 그 중에서도 놀라운 글을 보게되었다. 바로 Python 3.13에서는 GIL(Global Interpreter Lock)을 비활성화할 수 있다는 것이다. GIL은 Python의 큰 특징 중 하나이기도 했고, 대표적인 한계점이기도 했기 때문에 이 소식은 꽤나 놀라웠다.
GIL은 프로세스 별로 여러 스레드가 동시에 코드를 실행하지 못하도록 하여 스레드 실행을 동기화하는 매커니즘이다. GIL을 사용하는 인터프리터인 Python은 멀티코어 프로세서가 실행되어도 항상 하나의 스레드가 실행된다. 그래서 Python에서는 멀티 프로세싱 대신 멀티 스레딩 방식을 주로 사용한다. 위 글에서 소개한 GIL의 장단점은 아래와 같다.
GIL의 장점
- 구현의 단순성 : GIL은 Python 객체에 대한 동시 액세스를 방지하여 CPython의 메모리 관리를 단순화하며, 이를 통해 경쟁 조건 및 기타 스레딩 문제를 방지하는 데 도움이 될 수 있습니다.
- 단일 스레드 프로그램의 사용 편의성 : 단일 스레드 애플리케이션의 경우 GIL은 스레드 안전성을 관리하는 데 따른 오버헤드를 제거하여 간단하고 효율적인 코드 실행이 가능합니다.
- C 확장 기능과의 호환성 : GIL을 사용하면 복잡한 스레딩 모델을 구현하지 않고도 C 확장 기능을 작동할 수 있으므로 C 라이브러리와 인터페이스하는 Python 확장 기능의 개발이 간소화됩니다.
- I/O 바운드 작업의 성능 : I/O 바운드 애플리케이션에서 GIL은 성능을 크게 방해하지 않습니다. I/O 작업 중에 스레드를 전환하여 다른 스레드를 실행할 수 있기 때문입니다.
GIL의 단점
- 제한된 멀티스레딩 성능 : GIL은 한 번에 하나의 스레드만 Python 바이트코드를 실행하도록 허용하므로 CPU에 집중된 멀티스레딩 애플리케이션의 성능을 심각하게 제한할 수 있으며, 이는 멀티코어 프로세서의 활용도 저하로 이어질 수 있습니다.
- 스레드 관리의 복잡성 : GIL은 메모리 관리를 간소화하지만 동시적 애플리케이션의 설계를 복잡하게 만들어 개발자가 스레드 문제를 신중하게 관리하거나 대신 멀티프로세싱을 사용해야 할 수도 있습니다.
- 병렬 처리의 방해 요소 : GIL을 활성화하면 Python 애플리케이션에서 진정한 병렬 처리를 구현하는 것이 어려워서 개발자가 멀티코어 아키텍처를 효과적으로 활용하기 어렵습니다.
- 컨텍스트 전환의 비효율성 : GIL로 인한 빈번한 컨텍스트 전환은 오버헤드를 유발할 수 있으며, 특히 스레드가 많은 애플리케이션에서 성능 저하로 이어질 수 있습니다.
Python 3.13에서의 GIL
python 3.13 버전에서의 GIL에 대한 기능이 아래와 같이 추가되었다.
- 자유 스레드 모드에 대한 실험적 지원 : Python 3.13은 GIL을 비활성화할 수 있는 실험적 모드를 도입합니다. 이는 멀티스레딩 기능을 개선하고 CPU 바운드 작업에서 더 나은 성능을 가능하게 하는 것을 목표로 합니다.
- 특수 인터프리터 향상 : 특수 인터프리터는 GIL 없이 스레드 안전을 보장하기 위해 수정되었습니다. 여기에는 동시 특수화를 방지하기 위해 뮤텍스를 사용하고 일관된 캐시된 값을 보장하는 것이 포함됩니다.
- 새로운 Py_mod_gil 슬롯 : 확장 기능은 이제 모듈을 로드할 때 GIL 동작을 관리하기 위해 새로운 PEP 489 스타일 Py_mod_gil슬롯을 정의할 수 있습니다. 이 슬롯이 제대로 설정되지 않으면 인터프리터는 GIL을 활성화하고 모든 스레드를 일시 중지하여 사용자에게 경고를 제공합니다.
- PYTHONGIL 환경 변수 : 사용자는 환경 변수를 사용하여 런타임에 GIL 동작을 제어할 수 있습니다. PYTHONGIL 환경변수를 0으로 설정하면 GIL이 비활성화 상태로 유지되고, 1로 설정하면 활성화됩니다.
- 비세대 가비지 컬렉션(Non-Generational Garbage Collection) : GIL 변경 사항은 세대별 순환 가비지 컬렉션에서 비세대 모델로의 전환을 지원하여 가비지 컬렉션 주기 동안 스레드 일시 중지를 줄이고 멀티 스레딩 효율성을 개선하는 것을 목표로 합니다.
테스트해보기
Python3.13의 GIL 비활성화 기능은 아직 테스트 버전에서만 작동할 수 있기 때문에, python3.13이 아닌 python3.13t 버전을 사용해야 한다. python3.13t를 설치하는 방법은 아래 접은 글에 기술되어 있다.
# python3.13t 설치
# 소스 코드 다운로드
git clone https://github.com/python/cpython.git
cd cpython
git checkout 3.13
# 빌드 설정 (GIL 비활성화 옵션 포함)
./configure --enable-optimizations --disable-gil
sudo make -j $(nproc)
# 설치
sudo make altinstall
import datetime
import sys
from multiprocessing import Process
from threading import Thread
from typing import Any, Callable
def log_time(func: Callable[..., Any]) -> Callable[..., Any]:
"""
A decorator that logs the execution time of a function.
:param func: The function to be decorated.
:return: The wrapped function with execution time logging.
"""
def wrapper(*args: Any, **kwargs: Any) -> Any:
start_time = datetime.datetime.now()
result = func(*args, **kwargs)
execution_time = datetime.datetime.now() - start_time
print(
f"Function '{func.__name__}' executed in {execution_time.total_seconds()} seconds."
)
return result
return wrapper
def do_something(n: int = 1) -> int:
"""
Computes the n-th Fibonacci number.
:param n: The position in the Fibonacci sequence to compute (default is 1).
The first Fibonacci number is at position 1.
:return: The n-th Fibonacci number.
"""
a, b = 0, 1
for _ in range(n - 1):
a, b = b, a + b
return a
@log_time
def run_multi_thread_task(func: Callable[[Any], Any], input_data: list[Any]) -> None:
"""
Executes a function in multiple threads concurrently.
:param func: The function to execute, taking one argument.
:param input_data: A list of input data that will be passed to the function.
:return: None
"""
threads = []
for data in input_data:
thread = Thread(target=func, args=(data,))
threads.append(thread)
thread.start()
for thread in threads:
thread.join()
@log_time
def run_multi_processing_task(func: Callable[[Any], Any], input_data: list[Any]) -> None:
"""
Executes a function in multiple processes concurrently.
:param func: The function to execute, taking one argument.
:param input_data: A list of input data that will be passed to the function.
:return: None
"""
processes = []
for data in input_data:
process = Process(target=func, args=(data,))
processes.append(process)
process.start()
for process in processes:
process.join()
@log_time
def run_single_thread_task(func: Callable[[Any], Any], input_data: list[Any]) -> None:
"""
Executes a function in one thread.
:param func: The function to execute, taking one argument.
:param input_data: A list of input data that will be passed to the function.
:return: None
"""
for data in input_data:
func(data)
def main(func: Callable[[Any], Any], input_data: list[Any]) -> None:
run_single_thread_task(func=func, input_data=input_data)
run_multi_processing_task(func=func, input_data=input_data)
run_multi_thread_task(func=func, input_data=input_data)
if __name__ == "__main__":
print(f"Current python v: {sys.version}")
status = False
if sys.version_info == (3, 13):
status = sys._is_gil_enabled()
print(f"Global Interpreter Lock status: {"disabled" if not status else "enabled"}")
test_data = [400000] * 5
main(
func=do_something,
input_data=test_data,
)
위 글에서 제공한 테스트 코드를 통해 Python 3.13과 3.12버전 간 GIL에 대해 얼마나 달라졌는지 알아보자.
# python3.12 버전
Current python v: 3.12.0 | packaged by Anaconda, Inc. | (main, Oct 2 2023, 17:29:18) [GCC 11.2.0]
Global Interpreter Lock status: enabled
Function 'run_single_thread_task' executed in 5.410144 seconds.
Function 'run_multi_processing_task' executed in 1.119341 seconds.
Function 'run_multi_thread_task' executed in 5.859033 seconds.
# python 3.13 (GIL 활성화 상태)
Current python v: 3.13.0 | packaged by Anaconda, Inc. | (main, Oct 7 2024, 21:29:38) [GCC 11.2.0]
Global Interpreter Lock status: enabled
Function 'run_single_thread_task' executed in 5.458232 seconds.
Function 'run_multi_processing_task' executed in 1.125929 seconds.
Function 'run_multi_thread_task' executed in 5.736521 seconds.
# Python3.13t (GIL 비활성화 상태)
Current python v: 3.13.2+ experimental free-threading build (heads/3.13:90fc6117da5, Feb 25 2025, 17:17:35) [GCC 13.3.0]
Global Interpreter Lock status: disabled
Function 'run_single_thread_task' executed in 6.825225 seconds.
Function 'run_multi_processing_task' executed in 1.396384 seconds.
Function 'run_multi_thread_task' executed in 1.376363 seconds.
위 파일 실행 결과를 보면, GIL 비활성화 효과를 제대로 파악할 수 있다. GIL을 비활성화할 경우, 싱글 스레드 작업과 멀티 프로세싱 작업은 비슷하지만 멀티 스레드 작업에서 상당한 개선 효과를 보여준다. 3.12버전과 3.13 GIL 활성화 상태에서는 5.7~8초 정도의 시간을 보였지만, 3.13 GIL 비활성화 상태에서는 1.3초만에 작업이 완료되었다.
GIL 비활성화 버전은 아직 정식 배포된 것은 아니지만, 조금 더 안정될 경우 AI나 ML 부문에서 아주 좋은 옵션으로 사용될 것 같다. GIL 비활성화 기능이 정식으로 포함된 3.13버전이 배포된다면, 더 많이 써봐야할 것 같다.