파이썬(python) - GIL(Global Interpreter Lock)
GIL(Global Interpreter Lock) 이란?
GIL은 파이썬에만 존재하는 독특한 개념으로 파이썬에서 멀티스레딩을 할 때 다수의 스레드가 동시에 파이썬 바이트 코드를 실행하지 못하게 막는 일종의 뮤텍스(Mutex)입니다.
파이썬으로 작성된 프로세스는 한 시점에 하나의 스레드에만 모든 자원을 할당하고 다른 스레드는 접근할 수 없게 막아버리는데, 이 역할을 GIL이 수행합니다.
즉 멀티스레딩을 하더라도 파이썬에선 우리가 생각하는 것처럼 여러 스레드가 동시에 작업을 하진 않습니다.
깊게 파고들면 복잡하고 꽤 어려운 내용이니 최대한 간소화해서 핵심만 설명드리겠습니다.
파이썬에서의 멀티스레딩
일반적으로 멀티스레딩이라 하면 다음과 같은 상황을 기대합니다.
말 그대로 다수의 스레드가 동시에 각자의 작업을 수행하는 상황을 기대하게 됩니다.
하지만 파이썬에서는 GIL 때문에 다음과 같은 상황이 펼쳐집니다.
GIL이 스레드끼리 공유하는 프로세스의 자원을 이름 그대로 Global 하게 Lock 해버리고 단 하나의 스레드에만 이 자원에 접근하는 것을 허용합니다.
따라서 그림과 같이 멀티스레드라 하더라도 한 번에 하나의 스레드만 실행하게 됩니다.
이는 스레드 간에 컨텍스트 스위칭 비용을 발생시키고, 멀티스레드가 싱글스레드와 비슷한 성능을 보이거나 오히려 떨어지게 되는 결과를 만듭니다.
(여럿이서 일을 할 때보다 혼자서 일을 할 때 능률이 향상된다니 파이썬은 진정한 아싸가 아닐까요)
그렇다면 파이썬에서 굳이 멀티스레딩을 할 필요가 없나요?
물론 그렇다고 해서 멀티스레딩이 아예 의미 없는 것은 아닙니다.
CPU 연산이 큰 비중을 차지하는 경우는 상기한 것 처럼 멀티스레딩의 성능이 싱글스레드보다 떨어지게 됩니다.
하지만 I/O작업이 큰 비중을 차지하거나 sleep으로 일정 시간 대기해야 하는 경우 멀티스레딩이 더 좋은 성능을 보이게 됩니다. 이는 입력 대기시간이나 sleep으로 대기하는 동안 컨텍스트 스위칭이 이루어지기 때문입니다.
얘는 대체 왜 필요한건가
그렇다면 당연한 의문이 생깁니다. 대체 GIL을 왜 사용하는걸까요? 쓸데없이 멀티스레딩 성능만 저하시키는 천덕꾸러기 처럼 보이는데 말이죠..
이를 이해하려면 파이썬에서 메모리를 관리하는 방법에 대해 알아야 합니다.
여러분들도 아시다시피 파이썬에 존재하는 모든 것은 객체입니다.
그리고 파이썬은 이러한 객체들에 대해 Reference count(참조 횟수)를 저장하고 있습니다. 이 값은 각 객체들이 참조되는 횟수를 나타내며, 참조 여부에 따라 알아서 증감됩니다.
어떤 객체에 대한 모든 참조가 해제되어 Reference count가 0이 된다면, 파이썬의 GC(Garbage Collector)가 그 객체를 메모리에서 삭제시킵니다.
따라서 Reference count의 값은 항상 정확해야 적절하게 GC가 처리할 수 있겠죠?
그런데 만약 여러 스레드에서 동시에 한 객체에 접근하게 되면 어떻게 될까요? 그렇게 되면 객체의 Reference count에 대해 레이스 컨디션(Race Condition, 하나의 자원을 동시에 사용하게 될 때 기대하지 않은 결과가 발생하는 상황)이 발생하게 되고 이는 GC의 부적절한 행동을 야기할 수 있습니다.
이런 잔학무도한 상황을 방지하기 위해 파이썬은 GIL을 도입하게 된 것입니다. 애초에 한 스레드만 자원에 접근이 가능하다면 Race Condition이 발생할 일은 없게 되니까요.
대처법
GIL의 방해를 받지 않고 병렬 연산을 하기 위한 가장 대표적인 방법은 멀티 프로세싱을 사용하는 것입니다. 컨텍스트 스위칭 비용이 크다는 단점이 있지만 프로세스는 각자 독자적인 메모리를 가지기에 GIL의 영향을 받지 않습니다.
이 외에도 CPython이 아닌 다른 인터프리터를 사용하는 방법 또한 가능합니다. GIL은 CPython에서만 사용되기 때문입니다.