GIL이 무엇인지 안다는 가정하에 적은 글입니다.
GIL 알아보기
Cpython은 C와 다르게 메모리 관리를 직접할 필요가 없다. 기본적으로 reference counting
으로 메모리 관리를 한다. 예를 들어 보자.
처음 x가 참조되면 ref_cnt
는 1이 된다. y도 x를 참조하니 2가 되고 del x
, del y
연산이 수행 될 때 마다 ref_cnt
는 1씩 줄어든다. 0이 되면 메모리가 해제된다.
import ctypes
import gc
def ref_count(address):
return ctypes.c_long.from_address(address).value
def object_exists(object_id):
for object in gc.get_objects():
if id(object) == object_id:
return True
return False
x = object()
x_id = id(x)
print(ref_count(x_id)) # 1
y = x
print(ref_count(x_id)) # 2
del x
print(ref_count(x_id)) # 1
문제는 순환참조가 발생할 경우 reference counting
으로 해결을 못 하는 경우가 생긴다는 것이다. 아래 코드를 보자
class A:
def __init__(self):
self.b = B(self)
print(f'A: {hex(id(self))}, B: {hex(id(self.b))}')
class B:
def __init__(self, a):
self.a = a
print(f'B: {hex(id(self))}, A: {hex(id(self.a))}')
gc.disable() # GC 끄기
a = A()
a_id = id(a)
b_id = id(a.b)
print(ref_count(a_id)) # 2
print(ref_count(b_id)) # 1
print(object_exists(a_id)) # True
print(object_exists(b_id)) # True
a = None
print(ref_count(a_id)) # 1
print(ref_count(b_id)) # 1
print(object_exists(a_id)) # True
print(object_exists(b_id)) # True
a
는 a.b
를 a.b
는 a
를 순환참조 하고 있다. a를 None
으로 한다면 어디서도 a
와 a.b
에 어느 변수도 접근하지 못 하는데 코드를 보면 메모리를 차지하고 있다. 여기서 GC를 켜보면 정상적으로 메모리가 해제된다.
gc.collect() # GC 켜기
print(object_exists(a_id)) # False
print(object_exists(b_id)) # False
# reference count
print(ref_count(a_id)) # 0
print(ref_count(b_id)) # 0
대략적으로 Cpython의 Garbage collection에 대해 알아봤다. 그런데 reference counting
을 보면 동기화가 떠오르지 않는가? 저 연산이 atomic
하지 않으면?? 메모리 관리가 전혀 안 되겠는데? 그래서 바로 Cpython에는 GIL
이 나온 것! 모든 객체에 Mutex
를 걸 수는 없으니 쓰레드 자체에 Mutex
를 걸어 버렸다.