안녕하세요, 개발자 하댑입니다.
오늘은 파이썬 멀티스레드를 사용하려고 한다면 알아야하는 GIL에 대해 알아보겠습니다.
파이썬의 경우 단순히 멀티스레딩을 코드로 구현해서 테스팅을 해보면 멀티스레딩의 연산 속도가 성능이 나쁜 것을 확인할 수 있습니다. 이런 결과가 나오는 이유는 바로 CPython Global Interpreter Lock, 즉 GIL이 적용되기 때문입니다.
Global Interpreter Lock, GIL이 뭔데?
Python Wiki에서는 다음과 같이 설명합니다.
CPython에서 GIL은 파이썬 코드(bytecode)를 실행할 때에 여러 스레드를 사용할 경우, 단 하나의 스레드만 파이썬 객체에 접근할 수 있도록 제한하는 mutex 이다. 그리고 이 lock이 필요한 이유는 CPython이 메모리를 관리하는 방법이 thread-safe하지 않기 때문이다.
여기서 익숙하지 않은 두가지 개념인 thread-safe와 mutex에 대해서 간단하게 요약해 보겠습니다.
운영체제가 생성하는 작업 단위는 프로세스라고 합니다. 이 프로세스 안에서 공유되는 메모리를 바탕으로 여러 작업을 생성할 수 있는데, 이때 작업 단위를 스레드라고 합니다. 각 스레드마다 할당된 메모리가 있고, 스레드가 속한 프로세스가 가지는 메모리에도 접근할 수 있습니다. "thread-safe"하지 않다는 것은 위에 설명했던 것처럼 프로세스가 공유하는 메모리에 여러 스레드가 동시에 접근해서 정보를 변경할 경우 어떤 스레드의 작업 결과는 반영되지 않을 수 있다는 의미입니다. 따라서 이런 참사를 막기 위해 "mutex(mutual exclusion)"가 필요합니다. 한 스레드가 작업을 하는 동안에는 메모리 리소스에 접근을 제한하면 작업 결과가 누락되는 일을 막을 수 있을 겁니다. 예시를 통한 좀 더 자세한 설명을 원하면 이 블로그 포스팅을 참고해주세요.
그러니까 GIL은 파이썬에서 멀티스레드 작업을 할 경우에 병렬적으로 일을 하는 것이 아니라, 하나의 스레드에게 모든 자원을 허락하고, 그 이후에는 Lock을 걸어서 다른 스레드는 실행할 수 없도록 막는다는 뜻입니다.
GIL을 굳이 왜 사용하는데?
파이썬은 기본적으로 garbage collection과 reference counting을 통해 할당된 메모리를 관리합니다. 파이썬의 모든 객체는 reference count, 즉 해당 변수가 참조된 수를 저장하고 있습니다. 멀티스레드의 경우 여러 스레드가 하나의 객체를 사용한다면 reference count를 관리하기 위해 모든 객체에 대한 lock이 필요하게 됩니다. 이런 비효율을 막기 위해 GIL을 사용하게 되었습니다. 즉, reference count 동기화 문제를 해결하기 위해서 사용합니다. 사실 이런 간단한 설명으로는 이해가 되지 않기 때문에 조금 더 자세하게 개념을 하나씩 이해해 가면서 설명해보겠습니다.
CPython의 메모리 관리: Referece Counting
GIL을 이해하기 위해서는 파이썬에서 메모리 관리에 사용하고 있는 참조 카운팅(reference counting) 개념을 알아야 합니다. 참조 카운팅이 작동하는 방식을 보여주기 위해 간단한 코드 예제를 살펴보겠습니다.
import sys
a = []
b = a
sys.getrefcount(a) // 3
a
가 처음 만들어졌을 때의 reference 개수가 하나b
에a
의 reference를 할당했으므로, 그 개수가 하나 늘어나서 두 개sys.getrefcount
함수에 argument로a
가 들어가서, 이 함수 내부에서a
의 reference 개수를 하나 늘리므로 세 개 (그리고 이 함수가 끝날 때 다시 reference 개수를 하나 줄일 것이다)
최종 출력으로 나오는 a
의 reference 개수는 세 개가 되고, 이 개수가 0이 되면 CPython이 알아서 메모리를 회수한다고 생각할 수 있습니다. 두개 이상의 스레드가 만약 reference의 개수를 증가시키거나 감소시키면 메모리 유실이 일어나거나, 객체가 아직 존재하는데 메모리를 회수하는 등의 "이상한" 버그가 발생할 수 있습니다.
이 reference count 값은 스레드가 공유하는 전체 데이터 구조를 잠궈버리면 방지할 수는 있습니다. 다만 이렇게 여러 객체와 그룹에 멀티 잠금을 걸었다가 풀면 deadlocks이라고 하는 문제가 발생할 수 있습니다. 즉, 퍼포먼스 저하가 일어난다는 것이죠. 따라서 파이썬은 reference 개수를 일일히 보호하기 보다는 python을 실행시키기 위해서는 반드시 필요한 interpreter 자체를 잠그기로 결정합니다. 이로써 데드락과 같이 오버헤드가 발생하는 참사는 방지하지만, CPU-바인딩 파이썬 프로그램을 싱글 스레드화하는 효과가 있습니다.
GIL은 Ruby 같은 다른 언어의 인터프리터에도 사용되지만 유일한 솔루션은 아닙니다. garbage collection을 사용할 수도 있고, 싱글 스레드의 성능을 보완하기 위해서 JIT 컴파일러 같은 성능 향상 기능을 추가해야 하기도 합니다.
다른 방법도 있다며. 근데 파이썬은 왜 GIL인데?
파이썬은 사실 운영체제에 스레드 개념이 없던 시절에 등장했습니다. 파이썬은 많은 개발자들이 빠르고 쉽게 사용할 수 있게 만드는게 목표였기 때문에, 기존에 작성되어 있는 C 라이브러리를 가져다가 많이 쓰고 있었습니다. 거대한 커뮤니티에서 만들어낸 여러 C 라이브러리들을 새로운 메모리 관리 방법에 맞춰서 바꾸기 보다는 파이썬이 GIL을 도입해서 문제를 해결하게 된겁니다.
앞으로도 파이썬은 GIL을 사용할까?
초창기에 만들어진 CPython의 GIL이 Python 3가 되도록 계속 사용하고 있습니다.
파이썬 창시자이자 BDFL인 Guido van Rossum은 2007년 9월 자신의 기사에서 커뮤니티에 이렇게 얘기했습니다.
I’d welcome a set of patches into Py3k only if the performance for a single-threaded program (and for a multi-threaded but I/O-bound program) does not decrease.
단일 thread 프로그램에서의 성능을 저하시키지 않고 GIL의 문제점을 개선할 수 있다면, 나는 그 개선안을 기꺼이 받아들일 것이다.
아직 GIL 자체를 대신할 대안이 등장하진 않았지만, Python3는 기존 GIL을 개선했다고 합니다.
파이썬의 미래는... 과연?
파이썬 창시자 귀도 반 로썸이 python 4.x는 없을거라고 한만큼 이후 파이썬 버전에서도 GIL이 해결될 가능성은 없다는 의견이 대부분 입니다. 이런 이유 때문에 파이썬이 미래 프로그래밍 언어에서 밀려날거라는 예측이 나오고 있습니다. 여러 커뮤니티나 포럼에서 GIL이나 성능을 주요 문제점으로 꼽고 있는데, 하드웨어 성능의 향상으로 치명적인 단점이 아니라는 의견도 있습니다.
파이썬 사용자 입장에서 GIL이 문제가 되지 않는다고 주장하는 이유는 다음과 같습니다.
- 일단 단일 thread일 때는 아무런 문제가 없다.
- CPU가 바쁘게 계산하는 대신에 numpy/scipy 를 사용하면 효율적인 C코드로 연산처리하면 된다.
- 병렬 처리를 하는건 굳이 thread가 아니더라도 multiprocessing이나 asyncio 등 다른 선택지가 있다.
- 반드시 thread 동시적 처리가 필요하다면 Python implementation을 고려해볼 수 있다. Jython, IronPython, Stackless Python, PyPy 등이 있다.
저는 지금까지 Python, C/C++, Java, Go, Javascript, Typescript, Dart 등을 접해봤습니다. 파이썬이 가장 먼저 배운 언어인만큼 정이 많이 들었지만 GIL을 오늘까지 모를만큼 언어에 대한 이해가 부족했다는 생각이 드네요. Go, Rust, Julia 등 새로운 언어는 계속 등장하고 있고, 새로운게 나오는 속도도 갈수록 더 빨라지는 것 같습니다. 처음 개발을 배울 때는 앞으로 어떤 언어가 대세가 될지, 뭐를 배워야 좋을지 고민을 많이 했는데요, 사실 IT 산업에서 일을 하면서 내가 어떤 역할을 해나가고 싶으냐에 따라, 그리고 어떤 개발자가 되고 싶은지에 따라 언어는 그저 수단일 수 있다고 생각합니다. 제가 질문을 여기저기 하고 다닐때는 하나의 언어를 깊이 파라고 말씀하시는 선배님들이 많았는데요, 결국 어떤 언어든 만들어진 이유를 파다보면 자연스럽게 기술을 깊게 배워나가게 되기 때문인것 같습니다. 하지만 한번에 모든걸 소화하려고 하다보면 체하니, 기회가 닿을 때마다 조금씩 더 채워나간다는 공부한 내용을 정리해보겠습니다.
참고 자료
- https://realpython.com/python-gil/
- https://www.datacamp.com/tutorial/python-global-interpreter-lock
- https://ssungkang.tistory.com/entry/python-GIL-Global-interpreter-Lock%EC%9D%80-%EB%AC%B4%EC%97%87%EC%9D%BC%EA%B9%8C
- https://dgkim5360.tistory.com/entry/understanding-the-global-interpreter-lock-of-cpython
'📒 Tech Note > 웹 프로그래밍' 카테고리의 다른 글
[JS] 메시지 큐와 이벤트 루프 (Message Queue and Event Loop) (0) | 2022.08.25 |
---|---|
[JS] 변수와 함수 정의 (0) | 2022.06.24 |
[HTTP/3] HTTP3 등장! HTTP1부터 HTTP3까지 살펴보기 (0) | 2020.08.18 |
[JS] 참고자료 모음 (0) | 2020.08.13 |
[JS] 정규식 패턴 [xyz]과 정규식 메소드 match (0) | 2020.08.12 |