GIL이란?
- 파이썬 인터프리터가 한 스레드만 하나의 바이트코드를 실행시킬 수 있도록 해주는 Lock
GIL의 목적?
파이썬은 기본적으로 레퍼런스 카운팅을 사용하는데,
이 레퍼런스 카운트 변수가 멀티스레드 환경에서 Race Condition을 야기할 수 있기 때문에
하나의 공통된 자원을 여러 스레드에서 접근하여 발생하는 이런 문제를 예방하기 위해서 GIL
사용
Critical Section
- 병렬프로그래밍에서 둘 이상의 스레드가 동시에 접근해서는 안되는 공유 자원을 접근하는 명령문 또는 코드의 일부 영역
Race Condition
- 두 개 이상의 프로세스(혹은 스레드)가 공통 자원을 병렬적으로 읽거나 쓰는 동작을 할 때,
공용 데이터에 대한 접근이 어떤 순서에 따라 이루어졌는지에 따라 실행 결과가 같지 않고 달라지는 상황을 말한다.
Mutex
보통 GIL을 Mutex로 사용. (Cpython 코드)
static void take_gil(PyThreadState *tstate) {
...
MUTEX_LOCK(gil->mutex):
...
while (_Py_atomic_load_relaxed(&gil->locked)) {
...
int timed_out = 0;
COND_TIMED_WAIT(gil->cond, gil->mutex, interval, timed_out);
...
}
...
}
GIL 사용 시 멀티스레드 성능?
- GIL에 의해 공유자원에 동시 접근하지 못해서 싱글 스레드와 유사하거나 overhead로 인해 더 느려질 수 있다.
그러나
, GIL을 사용하는 이유는 공유 자원에 접근해서 읽고 쓰는 것으로 인한 Race Condition 때문이므로, GIL을 사용하지 않고 다른 스레드가 CPU Bound Job을 수행할 수 있도록 GIL을 해제하고 I/O Bound Job(ex) download, print 작업 등)을 수행하여 이점을 볼 수 있다.
멀티 스레드 대신 멀티 프로세스를 사용한다면?
- 스레드에 비해 프로세스는 무거운 자원이므로 부담스러울 수 있다.
- 남용한다면 overhead로 인해 더 많은 시간이 소요될 수 있다.
C-Bindings
- 동일하게 GIL에 의해 Lock이 걸리므로 성능 상의 이점을 보기 어렵다.
- 다른 스레드가 다른 작업을 수행할 수 없으므로 직접 해제를 해야 한다.
해결 방법
1. Per-Interpreter (PEP 684)
- 인터프리터를 여러 개 사용.
- 프로세스와 유사해보일 수 있으나, Process는 OS 자원이므로 커널과 유저 스페이스의 Context Switching으로 많은 비용을 요구하지만
인터프리터는 파이썬 런타임 내에 존재하는 개념이므로 상대적으로 Context Switching을 줄일 수 있다.
2. nogil flag
- 멀티스레드 환경에서 CPU Bound 작업 시에도 효율적으로 동작 - Atomic Operator 때
- Atomic Operator : 원자 단위로 작업 수행 가능
전자는 세 개의 단계로 이루어져서 중간에 다른 스레드가 해당 변수를 읽게 되면 문제가 발생할 수 있다.
반면, 후자인 Atomic Operator는 해당 문제를 해결할 수 있다.
(원자 단위 작업, LOCK ADD로 인해 Low Level에서 다른 스레드 접근 방지)
Reference)