Global Interpreter lock (GIL) 넌 누구냐?

5370 단어 pythonpython

intro

python 개발자라면 GIL이라는 개념을 모르면 안된다.
개인적으로 상대방이 파이썬에 대해 잘 아는지 물어볼때는 GIL을 세밀하게 설명해보라고 하면 필터링에 직빵이다. (내가 필터링된 경험 有) GIL은 단순하게 파이썬은 쓰레드 하나만 쓰는거 아니야? 라고 대답하는사람이 많을 것 같은데 왜 하나만 쓸까를 생각하면 꽤나 골치아픈 개념들이 튀어나온다. 그 개념들과 함께 정리하는 시간을 갖도록 하자

GIL이란?

GIL은 Global Interpreter Lock의 약자로 인터프리터 락을 걸어 쓰레드를 아무리 여러개 만들어 멀티 쓰레딩을 하더라도 한번에 하나의 쓰레드만 사용할 수 있도록 하는 제약이다.

왜? 자바처럼 그냥 여러개 훅훅 써서 멀티 프로세싱 멀티 쓰레딩 훅훅 해버려야 파이썬도 빨리 돌아가서 자바 C 같은 컴파일언어들 근처라도 따라갈거 아니야!!
파이썬은 성능을 포기하고 개발자의 편의성을 잡았다. (요즘은 Cpython, pypy 등으로 빠르긴 하지만 일부이고 C나 Go에 비비기엔 한참 멀리 있다..)
파이썬은 객체지향언어이다. 객체지향이란 우리가 사는 세상들이 전부 물건으로 되어 있고 우리가 보는 세상과 우리가 보는 코드를 최대한 비슷하게 표현하기 위해 object라는 개념을 넣어 모든 것들을 객체로 보고 코딩하는게 객체지향이다. 그리고 하나 더 중요한 개념을 알아야 하는데 python의 가비지 콜렉터 (garbage collector)의 동작원리를 알아야한다.
C계열의 언어는 직접 메모리 관리를 해주어야 한다. malloc으로 할당하고 다 쓰면 free를 해주는 번거로움 정말 미치게 만든다. 이걸 가비지 콜렉터가 대신 해주는데 언제 할당하고 언제 해제할지의 기준은 언어마다 다르다.

python의 가비지 콜렉터

python의 가비지 콜렉터는 reference counting이라는걸 활용한다. 모든 객체는 각자 내부적으로 reference count를 세고 있다.

import sys

class Sample:
    def __init__(self):
        self.a = 1

>>> obj1 = Sample()
>>> sys.getrefcount(obj1)
2

sys 패키지의 getrefcount라는 메서드를 활용하면 그 객체가 참조하고 있는 객체의 reference count를 return 해준다.
Q) 한번 할당해서 obj1만 가리키고 있는데 왜 2가 return 되냐?
A) sys.getrefcount 하면서 한번 더 참조해서 2가 return ㅇㅋ?

못믿겠는 사람은 아래 c언어로 구현된 python을 보고 _py_reftotal을 검색해 증가되고 감소되는걸 두 눈으로 보거나 아니면 나를 믿도록..
https://github.com/python/cpython/blob/main/Include/object.h

또한 순환참조라는걸 조심 해야 한다. 순환참조는 메모리 leak과 관련있으므로 매우 중요하지만 가비지 컬렉터 post가 아니니 이건 pass

>>> import gc
>>> gc
<module 'gc' (built-in)>
>>> gc.get_threshold()
(700, 10, 10)

파이썬에는 gc 라는 가비지컬렉터 built-in method가 있다.
가비지 컬렉터의 정보를 가져오는 gc.get_threshold()를 하니 (700, 10, 10)이라는 값을 뱉는다.
이는 (generation1, generation2, generation3)이다. gen으로 짧게 부르겠다.
gen1은 700이라는 기준점(threshold)을 갖고 2, 3은 10, 10을 갖는다는 말이다.

모든 객체가 생성되면 일단 주소값을 generation1 공간으로 보낸다. 그리고 count를 세다가 gen1 threashold(700)를 넘어서면 그 객체들의 reference count가 0인 친구들을 전부 free 시켜준다. 그리고 남은 객체 주소들을 전부 다음 generation2로 보내고 generation2의 count를 하나 증가시켜준다.(여기서 알아둬야할게 generation2에 올라간 object 갯수를 세는게 아니고 generation1이 free를 시켜준 count를 세는것 이다. 이게 조금 헷갈림)
generation3은 gen2가 10까지 차서 threashold를 띵 하고 치면 그때 counting이 될 것 이다.
아무튼 중요한건 reference count를 이용해 GC가 동작한다는거 그거 하나만 가져가자
https://blog.winterjung.dev/2018/02/18/python-gc
위의 블로거분이 정말 잘 정리해주셨으니 위의 블로그를 참조해도 좋을 것 같다.

다시 GIL로 돌아와서

자 이제 우리는 두가지를 알았다. python은 객체지향 언어이며 참조 횟수를 통해 가비지 컬렉터가 동작한다.
여기서 참조 횟수를 세는게 문제다. 동시에 멀티 쓰레딩을 허용하게 되면 참조 횟수를 관리하면서 공유자원인 reference count를 위해 mutax나 semaphore를 걸어줘야 한다 (변수에는 mutax를 걸어줘야함) 그렇다면 모든 객체에 뮤텍스를 건다? 이건 우럭잡으러 필리핀 가는 꼴이다. 그럴바엔 그냥 한번에 하나 쓰레드만 쓰도록 해서 쓰레드 자체에 뮤텍스를 걸은 제약이 바로 GIL이다.
그렇다면 파이썬은 병렬 프로그래밍이 안되는 것 일까?
그렇다 parallel programming은 안되지만 pseudo parallel programming은 가능하다.
우리의 쓰레드는 24시간 365일 돌아가지 않는다. 명령이 들어오면 그에 따른 처리만 하고 잠자기모드로 돌아간다. 또한 파이썬코드보다 IO 작업들이 훨씬 더 많고 오래 걸린다. 그러니 유능한 이벤트큐만 있으면 병럴 처리 '처럼' 사용 가능하다.

outtro

이게 GIL 관련 포스트인지 GC 관련 포스트인지는 모르겠지만 GIL을 알려면 공유자원 문제, Garbage Collector의 동장방식, 객체지향 지식 등등 굵직굵직한 것 들을 알아야만 이해할 수 있다. 또한 하나의 쓰레드만 쓰도록 lock을 걸었지만 pseudo parallel programming을 이용한다는 개념은 나중에 파이썬의 악마 끝판왕인 Coroutine 에서 중요하게 다뤄지기 때문에 꼭 이해하고 넘어가야 한다고 생각한다.
나중에는 파이썬 비동기 프로그래밍인 코루틴에 대해 알아보도록 하자!

좋은 웹페이지 즐겨찾기