GIL-왜 적용했고, 왜 없어지는가?

Hyeon Soo·2024년 4월 29일

1. 드디어 삭제 수순에 들어간 GIL

파이썬의 발전을 위한 개발 제안 및 방향성을 문서화한 것이 Python Enhancement Proposal, 줄여서 PEP라고 하며, python.org에서 관련 내용들을 찾아 볼 수 있습니다. 자주 찾아보지는 않지만 우연찮게 최근 꽤 인상적인 제안이 수용되었던 것을 보았는데요.

https://discuss.python.org/t/pep-703-making-the-global-interpreter-lock-optional-in-cpython-acceptance/37075

이 제안서입니다. 제안서는 보편적으로 쓰이는 CPython 인터프리터의 GIL을 기본이 아닌 옵션으로 만들고, 최종적으로 이를 제거할 것이라는 내용을 담고 있습니다. 대강 요약하면

  1. 내부적으로 free-threaded build를 시범 작성할 것
  2. 위의 빌드를 선택 가능한 옵션으로 하여, 배포버전에 포함시킬 것
  3. 최종적으로, free-threaded build가 기본이 될 것.

이 변경에 대한 코멘트들을 찾아보면 누군가는 성능 향상에 대해 기대하고 있고, 누군가는 성능 저하에 대해 우려하는 등 상반되어보이는, 하지만 맞는 이야기들을 하고 있습니다. 이를 보면서 GIL이 무엇인지, 왜 있는지, 장점이 뭐고 문제는 뭔지 한번 정리해보면 좋을 것 같아 작성을 하게 되었습니다.

1. GIL 적용의 배경: 메모리의 할당과 해제, reference counting

기본적으로 프로그램이 실행되어 프로세스가 만들어지면, 메모리에 프로그램 실행에 필요한 데이터와 CPU가 처리해야할 작업 과정이 적재됩니다. 그 구성은 대강 아래 그림과 같습니다.

이때 TEXT 영역은 CPU가 실제로 해야할 작업들이 순서대로 들어있는 byte 코드, Data 영역은 프로그램에 사전선언된 정적 변수와 전역변수들이 적재됩니다.

메모리 관리에 있어서 중요한 영역은 stack영역과 heap영역으로, 그외의 모든 것들이 정의되는 영역입니다. 코드 내의 변수, 현재의 제어 흐름을 나타내는 함수 호출 스택 등이 이 영역들에 정의됩니다. 현대에서 쓰이는 거의 모든 고급 언어의 레퍼런스격인 C언어에서는 매개변수 할당에 있어서 따로 명시하지 않으면 stack영역에 적재하고, 사용자가 필요에 따라 할당하고 도중에 해제하고 싶으면 heap영역에 직접 할당 및 해제할 수 있도록 되어있습니다.

다만 파이썬은 메모리 할당에 있어서 조금 다른 전략을 사용합니다. 파이썬의 모든 변수는 C와 다르게 Class 형태로 정의되어 있습니다. 그래서 stack 영역에는 class의 형태가 저장되는데, 이때 인스턴스 생성시의 실제 값은 heap영역에 저장이 됩니다. 즉, stack영역에 할당된 class 인스턴스는 실질적으로 heap영역에 저장된 값의 주소를 가리키는 포인터가 저장되어 있습니다. 그림으로 표현하면 아래와 비슷하겠네요.

이런 구조에서, C와 달리 파이썬은 heap 메모리 영역을 프로그래머가 직접적으로 해제할 필요가 없도록 자동으로 메모리 관리를 합니다. 파이썬이 선택한 메모리 관리 기법은 Reference Counting 이라고 하는데, mark and sweep 알고리즘 기반 메모리 관리 기법과 달리 이 기법은 heap영역에 할당된 데이터를 아무도 참조되지 않으면 해제하는 방식입니다.

이를 위해 파이썬의 모든 객체는 드러나지 않지만 자기 자신을 참조하는 객체가 있는지 참조 값을 실시간으로 확인, 0이 되면 자동적으로 해제합니다. 위의 그림에서 화살표 하나가 생길 때마다 객체의 참조 값이 증가하고, 없어지면 감소하다가 0이되면 즉시 해제하는 방식이라고 생각하면 큰 무리 없습니다.

2. 멀티 스레드 환경의 문제와 GIL의 적용

즉, 파이썬의 메모리 관리가 정상적으로 작동하려면 참조값이 결점 없이 관리가 되어야 합니다. 참조하고 있는 객체가 있음에도 불구하고 참조값이 0으로 변경된다면, 데이터가 즉시 해제되기 때문에 프로그램에 오류가 발생할 수 밖에 없기 때문입니다.

그런데 이 특징은 멀티 스레드 환경에서 프로그래머에게 예상하기 힘든 문제를 불러옵니다. 흔히 race condition으로 부르는 문제인데, 여럿 실행된 스레드에서 제각각 데이터를 수정하여 다른 스레드 입장에서 데이터의 값을 신뢰할 수 없는 것을 말합니다. 멀티 스레드를 적용한 코드는 프로세스와 달리 text, data, heap영역을 공유하고, stack영역만을 따로 할당받아 사용하기 때문에 발생하는 문제입니다. 그림으로 표현하면 다음과 같습니다.

이러한 상황을 thread-safe하지 않다고 말하는데, 이 상황을 메모리 관리와 연결지으면 참조 값이 잘못 관리될 가능성이 매우 높음을 의미합니다. 서로 다른 스레드에서 참조하는 경우 양쪽 스레드의 참조 값을 모두 합하여 계산해야하지만, 실제로는 참조값을 개별 stack 단위로 관리하기 때문에 참조값이 실제와 다른 값으로 계산됩니다. 위의 그림에서 보듯이 양쪽 스레드가 종료되고나서 객체가 해제되어야 정상인데, 각 스레드에서 참조값이 따로 관리되다보니 최종적으로 참조값이 1이 되어버려 해제가 안 되는 문제입니다. 심지어 한 스레드에서 참조하고 있는 데이터가 다른 스레드에서는 참조값이 0이 되어 해제되어버리면 오류로 인해 해당 스레드와 다른 스레드, 끝내는 프로그램 자체가 다운되는 문제가 발생합니다. 문제는 참조값의 관리를 프로그래머가 임의로 조절하는 것이 불가능하기 때문에, 이런 오류는 디버깅하기가 매우 어렵습니다.

CPython은 이 문제를 해결하기 위해, 하나의 실행 흐름만이 바이트 코드를 실행할 수 있도록 잠가버리는 방법을 사용하였고, 이를 실행하기 위한 것이 GIL입니다. 프로세스가 CPU에 할당되어 스레드를 통해 작업을 실행하고 있다면, 락이 반환될 때까지 다른 스레드는 바이트코드에 접근할 수 없습니다. 즉, heap영역의 데이터가 동시에 참조되는 상황이 애초에 발생하지 않고 사실상 하나의 일관된 흐름으로 코드가 실행되기 때문에 메모리 관리 오류를 방지할 수 있게 된 것입니다. 이 상황에서 참조 값이 변하는 과정을 표현하면 다음과 같습니다.

3. GIL 적용의 근거

GIL을 통해 자동으로 메모리를 관리함에 있어 발생할 수 있었던 문제는 막았으나, 이 방식은 멀티 스레드의 사용 범주를 크게 제한하는 문제를 내포하고 있고 GIL 삭제를 원하는 목소리들의 근거도 대부분 이 점을 지적해왔습니다. 하지만 GIL을 적용할 당시의 컴퓨팅 환경과, Reference counting 기반 메모리 관리 방법이 함께 가져오는 장점이 가지는 이점도 제법 컸기 때문에 계속 유지가 되었습니다.

CPython 인터프리터가 작성될 당시의 CPU는 대부분 싱글 코어 프로세서였습니다. 이는 프로그램 프로세스가 멀티스레드로 작성되었고, 멀티 스레드를 CPU가 지원한다고 하더라도(싱글 코어-싱글 스레드 환경이면 더더욱), 실제 CPU의 코어가 한 시점에 처리할 수 있는 작업 흐름은 1개뿐이라는 의미입니다.

이 경우, 하나의 프로그램이 시작부터 종료까지 소요되는 시간은 오히려 멀티 스레드일 때 더 오래걸리는 경우가 많습니다. 이는 context swiching, 문맥 교환과정에서 조금이나마 소요되는 시간 때문입니다. 그림을 통해 보면 다음과 같습니다.

프로세스의 한 스레드에서 다른 스레드로의 작업 변경도 필연적으로 문맥 교환 과정에서 발생하는 시간이 발생하는데, 위 그림에서의 Task1, Task2, Task3이 다 같은 프로세스 안의 작업이라면, 문맥 교환이 없는 상황보다 문맥 교환 비용만 더 들어가는 상황이 발생합니다.

즉, 위와 비슷한 환경에서 프로그램의 실행 속도를 최대한 높이기 위해선 문맥 교환을 최소화할 필요가 있는데, reference counting 기법은 이 부분에 강점을 가지고 있습니다. 위에서 언급한 mark and sweep 알고리즘 기반 메모리 관리 기법은 일단 실행중인 모든 프로세스를 일시 중지한 다음에, 메모리 관리를 위한 프로세스를 할당하여 작업을 진행합니다. 즉, 문맥교환이 반드시 발생한다는 의미입니다. 반만 reference counting은 실시간으로 수행되기 때문에 메모리 관리를 위한 문맥 교환이 거의 필요 없습니다.

결과적으로, 멀티 스레드를 통해 얻을 수 있는 이점이 크지 않고, 속도 향상을 위해 reference counting 기반의 메모리 관리를 선택하면서 발생할 문제들을 최대한 간단히 처리하기 위해 GIL을 설치한 것입니다. 즉, 보편적인 환경에서 얻을 수 있는 이점이 많았기 때문인 것입니다.

이에 더하여, GIL은 모든 파이썬 프로그램의 동작에 걸리는 것이 아니라, 파이썬이 런타임 상태에 있을 때만 걸립니다. 즉, 작업들 가운데 런타임 상태에 있지 않은 작업 유형들은 멀티 스레드를 이용할 수 있기 때문에 당시에는 GIL을 이용하는 것이 충분한 합리성이 있었습니다. 예를 들어, time.sleep() 이라던가, requests 라이브러리의 외부 호출 같은 경우, sleep시간이 지날 때까지, requests 응답이 올 때까지 파이썬은 runtime 상태에서 벗어나있고, 이 상태에서는 GIL을 반납하기 때문에 바로 다른 스레드가 작업을 이어받을 수 있습니다.

4. 환경의 변화와 GIL

그런데 멀티코어, 멀티 스레드 CPU가 보편화되고, 대다수의 운영체제가 멀티 스레드를 최대한 효율적으로 운용할 수 있도록 변화했으며, hyper-threading을 이용한 하드웨어 수준에서도 스레드의 평행 실행이 가능해지기까지 하면서 GIL은 더 거센 비판에 직면했습니다.

위에서 살펴본 싱글코어 CPU 환경과 달리, 멀티코어 CPU가 보편화되자, 코어가 평행 스레드 운용일지원하기 이전부터 운영체제들은 한 프로세스의 스레드를 한 시점에 여러 코어에서 평행하게 수행함을 통해 소요 시간을 획기적으로 단축하기 시작했습니다. 그림으로 표현하자면 대략 아래와 같습니다.

한 프로세스를 4개의 스레드로 분할한 다음에, 4개의 코어에 각각 배분하면 한 코어에서 처리하는 것과는 달리 훨씬 빠르게 종료되는 것입니다.

그런데 파이썬은 한번에 하나의 제어 흐름만이 파이트 코드에 접근이 가능하기 때문에, 여러 코어가 동시에 한 프로세스의 스레드를 실행하는 것이 애초에 불가능합니다. 파이썬은 평행 프로그래밍을 수행하려면 필연적으로 멀티 프로세스 형태로 프로그램을 작성해야 했는데, 위에서 살펴본 것 처럼 스레드는 stack 영역만 따로 관리하기 때문에 문맥 교환이 훨씬 가볍고 스레드간 데이터 교환 및 통신이 용이한 반면, 프로세스의 전환은 바이트코드, data, stack, heap 모두가 문맥 교환이 필요하기 때문에 엄청나게 무겁습니다. 프로세스간 통신도 비용이 크기 때문에 더더욱 비교가 안 되는 상황이 된겁니다.

5. 아무튼 없어진다니까....

얼마 후에는 이 작업을 멀티 스레드로 가능한지 무의미한지 일일히 따질 필요는 없어지겠으나, 위에서 살펴본 것처럼 여러 이슈가 같이 엮여있는 주제이기 때문에... PEP 문서에도 명시하고 있긴하지만 이 문제들을 어떤 방식으로 해결할지 확인하는 것도 재미있을 것 같습니다.

1. Thread-safe, race condition문제를 어떻게 해결할 것인가?

결국 위의 문제는 메모리 관리 기법의 큰 변화를 암시합니다. Reference counting에서 아예 벗어날지, 아니면 위의 문제를 최대한 해소하도록 reference counting을 변경할지, 아니면 아예 프로그래머가 알아서 조심하도록 하게 할지 방법은 여럿있을 것 같습니다.

2. 위와 연결된 문제로, 싱글 스레드 환경에서의 성능 저하를 어떻게 할 것인가?

메모리 관리 기법을 갈아치우던 조정하던, 예전과는 달리 싱글 스레드 환경에서도 시간이 더 걸릴 수 밖에 없습니다. reference counting을 유지하더라도, 중간중간에 thread-safe한지 등을 체크하는 과정이 들어가야할 테니까요. 문서에서는 10-15% 정도의 성능 저하를 예상하고 있는데, 중간 중간 달린 코멘트에서는 그보다 더한 성능저하를 가끔 보고하는 경우도 있어서 추이를 지켜봐야 할 것 같습니다.

3. 호환성의 문제는?

아무리 GIL이 인터프리터 레벨의 이슈라고 하지만, 이러한 특성에 의존하고 있던 수많은 확장들도 이 환경에 새로 적응해야할 것입니다. 중요하지만 개발이 멈추어 있던 일부 확장들은 더 어렵겠죠.

어차피 장기적인 프로젝트이니만큼 매일매일 업데이트를 따라갈 필요야 없겠으나, 중간중간 문서를 확인해서 진행 상황의 파악과 관련된 이슈들을 공부해나가면 좋을 것 같네요.

0개의 댓글