공부 이야기/그냥 찾아보는 공부

mutex 개념으로 살펴보는 Python의 GIL

mind: 2025. 5. 2. 19:40

Global Interpreter Lock이라고 불리는 파이썬의 GIL은 CPython 레벨에서 파이썬 오브젝트에 대한 mutex 잠금 장치이다.

여기서 잠깐! mutex란?
mutex는 latch, lock과 다르게 매우 빠르고 가벼운 잠금 기법이다. 
-> 매우 빠르다는 뜻은, 잠금 획득부터 해제까지 소요되는 시간이 짧다는 뜻이고
-> 가볍다는 뜻은, 잠금 대상의 메모리 할당이 상대적으로 작다는 뜻이다.

오라클 기준으로, 블록보다는 행이, 행보다는 파라미터 변수가.
자바 기준으로, 클래스보다는 변수 하나가 메모리 할당이 작기 때문에 더 빠르고 가벼운 잠금이 가능하다.
mutex는 보통 이렇게 적게 메모리를 할당받는 객체에 대해 짧은 시간 동안만 잠금을 유지할 때 사용된다.

latch랑 lock처럼 명시적 acquire&release 과정이 존재하지 않고 CPU 클럭 단위로 발생하는 spin lock에 의해 잠금을 획득/해제 한다.
원자 단위의 트랜잭션으로 발생할 때 mutex의 성능이 보장된다.

GIL은 특정 시점에 오로지 하나의 쓰레드만 파이썬 바이트 코드를 실행할 수 있도록 통제를 한다.

 

GIL 메커니즘에 의해 특정 오브젝트에 다수의 쓰레드가 쏠리는 경우 acquire & release에 걸리는 부하로 I/O 성능에 지연이 발생할 수 있다.

 

파이썬이 GIL을 도입한 이유는 thread NOT safety 특성을 방지하기 위해 단순하게 stop or go 알고리즘으로 구현했기 때문이다.

(위 그림을 보면 특정 time에 단 하나의 쓰레드만이 run 상태임을 확인할 수 있다)

lock이 아닌, low level인 mutex 방식이므로 재빠르게 자원 반납이 될 것이라고 예상했던 모양이다.

또한 파이썬 개발 문서를 살펴보면 당시에는 멀티쓰레딩 개념이 흔하지 않았기에 성능보다는 안정성을 우선해서 개발한 것으로 보여진다.

오라클에서도 mutex는 cursor pin X (동일 sql id에 대한 재참조 wait event)에서도 확인할 수 있다.

 

이를 해결하기 위해 파이썬은 멀티프로세싱 기술을 지원한다.

쓰레드가 아닌 프로세스를 여러 개 띄워서 처리하면 GIL 메커니즘으로 발생하는 locking delay가 없기 때문이다.

물론 Context Switching이랑 프로세스 할당에 따른 메모리 오버헤드가 발생한다.

 

아니면 개발자가 직접 Lock이 발생할 때 높은 우선순위를 먼저 처리하도록 직접 로직을 구현하는 방법도 있다.

이는 Java, Kotiln에서 synchronized, atomicXXX으로 명시적 잠금을 하는 방식과 유사한 방법이다.

 

Reference Counting

파이썬의 GIL을 찾아보면 함께 등장하는 개념으로 파이썬의 객체 관리와 관련이 있다. 

Java와 대비되는 메커니즘이다.

Java의 경우는 JVM의 Garbage Collector가 특정 시점마다 stop-the-world, G1/ZGC로 미참조 객체를 회수하는 반면

파이썬의 경우 참조 횟수(reference count)가 0이 되면 즉각 회수한다.

객체 관리에 대한 동시성 제어를 자동으로 해주는 JVM과 다르게 파이썬은 즉각 회수할 때 쓰레드 충돌이 발생할 수 있다.

-> 쓰레드 충돌이 발생하는 경우? ex. 회수될 객체를 다시 사용하려는 자 vs 회수하려는 파이썬

따라서, 파이썬의 GIL을 무턱대고 없애면 위와 같은 상황에서 runtime error가 발생할 수 있다.

그래서 ref count가 GIL과 연관이 있어 등장하는게 아닐까.

 

결론) 파이썬으로 멀티쓰레드 로직을 개발할 때는 GIL 특성과 객체 회수 방식을 꼭 고려해야 한다.