GIL 에 대해서
GIL(Global Interpreter Lock)
CPython에서의 GIL은 Python 코드(bytecode)를 실행할 때에 여러 thread를 사용할 경우,
단 하나의 thread만이 Python object에 접근할 수 있도록 제한것입니다.
그리고 이 lock이 필요한 이유는 CPython이 메모리를 관리하는 방법이 thread-safe하지 않기 때문입니다.
우선 GIL 를 왜 사용하는지 알아보겠습니다.
GIL 사용 목적
파이썬에서 쓰레드을 여러 개 생성한다고 해서 실제로 여러 개의 쓰레드가 동시에 실행되는 것은 아닙니다.
정확히 말하자면 두 개의 쓰레드가 동시해 실행되는 것처럼 보이는것 일뿐,
특정 시점에서는 여러 개의 쓰레드 중 하나의 쓰레드만 실행됩니다.
이는 별도의 동기화 설정을 하지 않았더라도, 파이썬 언어가 원래 그렇게 동작하도록 의도적으로 설계되었기 때문입니다.
그 이유는 파이썬이 변수를 관리하는 방법에 있습니다.
파이썬은 객체를 reference count를 통해 관리한다.
예를 들어, 객체를 참조하는 다른 객체 또는 위치가 늘어날수록 해당 객체의 reference count는 증감하게 되며,
reference count가 0이 되면 객체는 메모리에서 해제됩니다.
이것이 바로 파이썬의 가비지 콜렉터 (Garbage Collector: GC) 기능입니다.
그런데 멀티 스레딩 환경에서 만약 각 스레드가 특정 객체를 사용하게 된다면 자바 같은 언어의 경우엔 특정 변수에만 동기화 처리를 해주면 되지만
파이썬은 객체의 메모리 관리하는방법의 특성상 모든객체에 일일이 Lock 을 걸어야하는 상황이 생겨버립니다.
파이썬 GIL 로 인한 성능 저하
파이썬에서 특정 시점에는 하나의 쓰레드만 실행이 되어야만 합니다.
단, 여러 개의 쓰레드가 실행될 때는 내부에서 변환된 파이썬의 바이트코드가 일정 단위 만큼 실행될 때마다 다른 쓰레드로 Context Switching이 발생한다
이는 Context Switching은 성능 오버헤드의 주범이기 떄문에, MPI와 같은 HPC 환경에서 파이썬을 사용하는 것은 그다지 권장하지않습니다. ㅁ 그 예로 CPU Bound Task 는 CPU 가 처리하면 더 빠른 작업들을 의미하는데,
CPU Bound Task 의 경우에는 파이썬에서 멀티스레드로 구현하는 경우에 같거나 더 느려집니다.
위에 말했다싶이 쓰레드간의 Lock 을 Acquire / Release 하는 과정에서 Context Switching 에 대한 오버헤드가 원인입니다.
만약 GIL 이 없다면 ?
import threading
x = []
def append_two(l):
l.append(2)
threading.Thread(target=append_two, args=(x,)).start()
x.append(1)
print(x)
해당 코드는 [2,1] 또는 [1,2] 를 출력하는 쉽게 예측할수없는 상태가 되어버립니다.
원래파이썬에서 list.append 는 원자적(atomic) 연산인데 만약 원자적 연산이 아니라면 메모리 간섭이 발생해
리스트의 값이 알수없는 상태가 되어버리고 맙니다.
이런 원자적 연산이 가능한 이유는 GIL를통해 한번에 하나의 스레드만 바이트 코드 명령을 실행 할수있기 때문입니다.
그렇기때문에 스레드 많은 바이트 코드를 실행한다면 GIL 를 얻기위한 경쟁이 심해지기 때문에 오히려 싱글 스레드 보다 속도가 느려질수있는상황이 발생하는것입니다.
파이썬에서 분산처리
파이썬으로 정 분산처리를 하고싶다면 파이썬 3.8에서는 여러개의 인터프리터를 실행해 각각의 GIL 를 가질수있다고합니다.
또한 파이썬에서 멀티스레딩은 금지! 라는것이 아니라 상황에 따라서는 스레드를 사용하는것이 답일때가 있을수있다고 생각합니다.
하지만 그 역시도 코드가 복잡해지면 복잡해질수록 멀티스레드로 인한 에러 발생확률도
높아지기 때문에 스레드 여러개 대신 프로세스 여러개를 사용하는 방법이 좋다고합니다.
Reference
https://m.blog.naver.com/alice_k106/221566619995
https://velog.io/@doondoony/Python-GIL
[서적] 실전 스케일링 파이썬