본문 바로가기

Development/Diary

[개발일기]Python은 멀티쓰레딩 언어인데 GIL은 왜멀티쓰레딩을 막도록 설계되었나요? 그리고 Python 3.12 Per-interpreter GIL

인턴 생활을 하면서 Python의 대해 공부를 좀 했었는데, Python은 멀티쓰레딩 언어임에도 GIL라는 장치는 마치 단일 쓰레딩만 가능하도록 설계가 되었다는 것을 알았습니다. 왜 멀티쓰레딩 언어임에도 이를 막고자 설계가 되었는지 시니어 개발자분께 여쭤본 기억이 있는데요,

마침 최근 Python 3.12에는 Interpreter improvements의 사항으로

  • PEP 684, a unique per-interpreter GIL

가 있더라구요!

 

이 글에서는 GIL과 per-interpreter GIL 에 대해 다루고자 합니다.

GIL

파이썬은 메모리 관리를 위해 참조 계수(reference counting)를 사용합니다. 이는 파이썬에서 생성된 객체가 해당 객체를 가리키는 참조의 개수를 추적하는 참조 계수 변수를 가지고 있다는 것을 의미합니다. 이 개수가 0이 되면 해당 객체가 사용 중인 메모리가 해제됩니다.

(참고로 자바에선 가비지 컬렉션이 더 이상 사용되지 않는 객체를 자동으로 해제하여 메모리를 회수하죠. GIL과는 동작이 다릅니다.)

참조 계수가 어떻게 작동하는지 보여주는 간단한 코드 예제를 살펴보겠습니다:

>>> import sys
>>> a = []
>>> b = a
>>> sys.getrefcount(a)
3


위의 예제에서 빈 리스트 객체 []의 참조 계수는 3이었습니다. 이 리스트 객체는 a, b 및 sys.getrefcount()에 의해 참조되었습니다.

이제 GIL에 대해 다시 돌아와보겠습니다. 문제는 이 참조 계수 변수가 두 개의 스레드가 동시에 값을 증가 또는 감소시키는 Race Condition 으로부터 보호가 필요하다는 것이었습니다. Race Condition이 발생하면 메모리 누수가 발생할 수 있거나 아직 해당 객체에 대한 참조가 존재하는 상황에서 메모리를 잘못 해제할 수 있습니다. 이로 인해 파이썬 프로그램에서 충돌이나 예기치 않은 버그가 발생할 수 있습니다.

이 참조 계수 변수는 공유 스레드 간에 수정이 불일치하게 이루어지지 않도록 모든 데이터 구조에 락(locks)을 추가하여 안전하게 보호할 수 있습니다. 그러나 모든 객체 또는 객체 그룹에 락을 추가하면 여러 개의 락이 존재하게 되어 데드락(deadlocks)과 성능 저하를 유발할 수 있습니다.

GIL은 인터프리터 자체에 대한 단일 락(lock)으로, 어떤 파이썬 바이트 코드의 실행이 인터프리터 락을 획득해야 한다는 규칙을 추가합니다. 이렇게 함으로써 데드락을 방지하고 성능 오버헤드를 크게 발생시키지 않습니다. 그러나 이로 인해 CPU 집약적인 파이썬 프로그램은 사실상 단일 쓰레드로 실행됩니다.


GIL의 영향은 단일 쓰레드 프로그램을 실행하는 개발자에게는 보이지 않지만 CPU 바운드 및 멀티스레드 코드에서 성능 병목이 될 수 있습니다.

GIL은 여러 CPU 코어를 갖는 멀티쓰레드 아키텍처에서도 한 번에 하나의 쓰레드만 실행될 수 있도록 하므로 GIL은 Python의 "악명 높은" 기능으로 알려져 있습니다.

Python은 멀티쓰레딩 언어인데 GIL은 왜 이를 멀티쓰레딩을 막도록 설계되었나요?

사실, GIL(Global Interpreter Lock) 설계 결정은 파이썬을 오늘날처럼 인기 있는 언어로 만드는 데 기여한 중요한 요소 중 하나였습니다. 파이썬은 쓰레드 개념이 없던 시절부터 존재했습니다. (이 부분이 저가 시니어 개발자분께 들었던 내용입니다. 알고보니 파이썬은 자바보다 오래된 언어였어요.) 파이썬은 개발을 보다 빠르게 진행하고자 쉽게 사용할 수 있도록 설계되었습니다.

기존 C 라이브러리에 대한 많은 확장 기능이 파이썬에서 필요로 했는데, 이러한 C 확장 기능은 일관되지 않은 변경을 방지하기 위해 Thread-safe한 메모리 관리를 필요로 했었고, 이를 GIL이 제공했습니다.

GIL은 간단하게 구현할 수 있었으며 파이썬에 쉽게 추가할 수 있었습니다. 이로써 단일 쓰레드 프로그램에 대한 성능 향상을 제공하며 하나의 잠금(lock)만 관리하면 되기 때문입니다.

Thread-safe하지 않은 C 라이브러리를 통합하는 것이 더 간단해졌고, 이러한 C 확장 기능은 파이썬이 다양한 커뮤니티에서 즉시 채택되는 이유 중 하나가 되었습니다.

요컨대, GIL은 파이썬 초기에 CPython 개발자들이 직면한 어려운 문제에 대한 실용적인 해결책이었습니다. 그리고 이 결정이 파이썬의 인기를 높이는 데 크게 기여했습니다.

Per-Interpreter-GIL

Per-interpreter-GIL를 도입함으로써, 파이썬은 여러 개의 인터프리터를 동시에 실행할 수 있게 되었습니다. 이는 멀티스레드 환경에서 각각의 인터프리터가 독립적으로 작동하므로써 GIL의 영향을 줄이고, 멀티코어 프로세서를 최대한 활용할 수 있게 합니다.

이 기능은 PEP 554(PEP: Python Enhancement Proposal)에 의해 제안되었으며, 

PEP 684는 인터프리터 당 하나의 GIL(Global Interpreter Lock)을 소개했습니다. 이로써 하위 인터프리터(sub-interpreters)가 독립적인 GIL을 가지도록 생성될 수 있습니다. 이것은 파이썬 프로그램이 다중 CPU 코어를 완전히 활용할 수 있도록 합니다. 현재 이 기능은 C-API를 통해서만 사용할 수 있지만, 파이썬 API가 3.13 버전에서 예정되어 있습니다.

자신만의 GIL을 가진 인터프리터를 생성하려면 Py_NewInterpreterFromConfig() 함수를 사용하면 됩니다.

PyInterpreterConfig config = {
    .check_multi_interp_extensions = 1,
    .gil = PyInterpreterConfig_OWN_GIL,
};
PyThreadState *tstate = NULL;
PyStatus status = Py_NewInterpreterFromConfig(&tstate, &config);
if (PyStatus_Exception(status)) {
    return -1;
}
/* The new interpreter is now active in the current thread. */


하위 인터프리터와 각각의 GIL을 사용하는 C-API를 어떻게 사용하는지에 대한 더 많은 예제는 

Modules/_xxsubinterpretersmodule.c. (Contributed by Eric Snow in gh-104210, etc.) 에서 참고 가능합니다.

 

참고자료

https://docs.python.org/3/whatsnew/3.12.html#pep-684-a-per-interpreter-gil

 

What’s New In Python 3.12

Editor, Adam Turner,. This article explains the new features in Python 3.12, compared to 3.11. Python 3.12 was released on October 2, 2023. For full details, see the changelog. Summary – Release hi...

docs.python.org

https://realpython.com/python-gil/

 

What Is the Python Global Interpreter Lock (GIL)? – Real Python

Python's Global Interpreter Lock or GIL, in simple words, is a mutex (or a lock) that allows only one thread to hold the control of the Python interpreter at any one time. In this article you'll learn how the GIL affects the performance of your Python prog

realpython.com