- 링크: realpython.com/python-gil/

GIL(Global Interpreter Lock)은 간단히 말해 뮤텍스로, 한 쓰레드만이 파이썬 인터프리터를 컨트롤할 수 있게 해주는 락입니다

즉 한 번에 한 쓰레드만 실행될 수 있다는 것이고, 이로 인해 CPU 성능이나 멀티쓰레드 코드에 병목이 되기도 합니다.

 

GIL의 목적은?

파이썬은 메모리 관리 방법으로 레퍼런스 카운팅(reference counting)을 사용합니다. 이는 파이썬 오브젝트마다 레퍼런스로 사용된 횟수를 저장하는 것이고, 0이 되는 경우 메모리에서 해제하는 방식입니다.

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

sys.getrefcount 함수로 레퍼런스를 확인할 수 있습니다. 위 코드에서 빈 배열([])은 a, b, sys.getrefcount에 의해 참조되고 있으니 refcount가 3인 것을 확인할 수 있습니다.

다시 GIL로 돌아오면, 레퍼런스 카운트의 문제점은 여러 쓰레드가 동시에 객체의 참조 카운트(refcount)를 수정할 수 있으니 race condition이 발생합니다. 그래서 이 문제를 막으려면 refcount를 수정하려고 할 때마다 lock을 걸어야 하는데 모든 객체에 lock을 걸어야 하는 것도 부담이고, 교착상태(deadlock)의 위험성도 있습니다.

따라서 이 문제를 해결하고자 단 하나의 lock, 인터프리터를 통째로 Lock을 거는 방식을 사용한것입니다. 이 방식은 deadlock을 방지할 수 있고 모든 변수에 lock을 거는 것으로 인한 성능 저하는 없어지지만, 모든 cpu 위에서 올라가는 프로그램을 single-threaded로 만든다는 단점이 있습니다.

HotSpot JVM의 메모리 사용방식

물론 GIL이 메모리 관리의 유일한 해결책은 아닙니다. 대표적으로 자바는 GC(Garbage Collection)를 사용합니다. (레퍼런스 카운팅은 상호참조가 되는 경우 메모리 누수 문제가 발생합니다)

 

왜 CPython에서 GIL이 선택됐을까?

CPython은 파이썬 구현체 중 가장 보편적으로 사용되는 구현체인데, GIL은 CPython 개발자가 파이썬 초기에 직면한 문제의 적당한 해결책이었다고 합니다

  • thread-safe하지 않은 기존 C 라이브러리를 쉽게 사용할 수 있음
  • GIL은 구현이 간단함
  • 하나의 lock -> 단일 쓰레드 프로그램 성능 향상

 

파이썬에서 멀티쓰레드 프로그램의 성능

프로그램은 CPU-bound, I/O-bound 프로그램으로 나누자면 CPU-bound 프로그램은 행렬곱, 이미지처리 등 CPU 코어를 최대한 활용하는 프로그램이고, I/O-bound 프로그램은 네트워크, 데이터베이스 등 입출력을 기다리는데 많은 시간을 소비하는 프로그램입니다.

I/O-bound 프로그램의 경우 GIL이 성능에 영향이 없습니다

하지만 CPU-bound 프로그램의 경우 GIL로 인해 멀티 쓰레드로 짠다고 해도 단일 쓰레드로 동작합니다. 멀티쓰레드로 구현하면 실제로 단일 쓰레드로 구현한 것보다 느려질 수 있는데, 그 이유는 lock으로 인한 오버헤드입니다.

 

GIL을 우회하는 법

가장 널리 사용되는 방식은 멀티쓰레드 대신 멀티프로세스를 사용하는 것입니다. multiprocessing 등의 모듈을 사용할 수 있는데, 프로세스는 독립된 메모리 할당이 필요하고 자체적 오버헤드가 있어 순수한 멀티쓰레드보다는 성능이 떨어질 수 있습니다

그 다음엔 Jython 등 GIL을 사용하지 않는 파이썬 구현체를 사용하는 방법도 있습니다만 많이 쓰이지는 않습니다

반응형