python 동시성 관리-What is a GIL?
Python GIL
GIL이란 Global Interpreter Lock의 약자로 여러 개의 스레드가 파이썬 코드를 동시에 실행하지 못하도록 하는 것이다.
즉, 인터프리터 락을 걸어 쓰레드를 아무리 여러개 만들어 멀티 쓰레딩을 하더라도 한번에 하나의 쓰레드만 사용할 수 있도록 하는 제약.
따라서 멀티 CPU 환경에서도 python thread는 어느 시점에나 1개의 스레드가 실행된다는 단점이 있다.
GIL은 CPython에만 존재
CPython은 python code를 bytecode로 변환한 뒤, interpreting을 진행.
따라서 compiler와 interpreter의 역할을 모두 수행한다.
대부분의 python의 내부는 CPython으로 이루어져 있어 이 글에서 python이라함은 CPython으로 이루어진 python을 의미한다.
Python의 메모리 관리
GIL의 존재 이유를 알기 위해서는 Python이 메모리 관리를 어떻게 하는지 알아야한다.
파이썬은 C 또는 C++과 같이 프로그래머가 직접 메모리를 관리하지 않고 레퍼런스 카운트(Reference Counts)와 가비지 콜렉션(Automatic Garbage Collection)에 의해 관리된다.
import sys
class RefExam():
def __init__(self):
print('create object')
a = RefExam()
print(f'count {sys.getrefcount(a)}')
b = a
print(f'count {sys.getrefcount(a)}')
c = a
print(f'count {sys.getrefcount(a)}')
c = 0
print(f'count {sys.getrefcount(a)}')
b = 0
print(f'count {sys.getrefcount(a)}')
"""
OUT PUT:
count 2
count 3
count 4
count 3
count 2
python은 모든 객체에 count를 포함하고 이 count는 객체가 참조될 때 증가하고 참조가 삭제될 때 감소하는 방식으로 작동한다. 이때 카운터가 0이되면 메모리 할당이 해제된다.
이런식으로 메모리를 관리하는데, 만일 여러 thread에서 같은 객체를 참조한다면 이를 더이상 추적하기 어려워진다.
이를 추적하기 위해서는 RC 정보를 변경할 때마다 lock을 걸어야 하는데, 이렇게 되면 성능문제가 발생할 수 있다.
따라서 하나의 thread만 python 객체에 접근할 수 있도록 lock을 거는 것이다.
GIL은 CPU bound vs IO bound??
GIL이 영향을 미치는 범위가 CPU bound일 때와 IO bound일 때가 다르다는 말을 들어본 적이 있을 것이다.
하지만 이는 이 문제가 아닌, 수행되어야 하는 어떤 job이 python runtime과 상호작용하면 GIL의 영향을 맏고 상화작용하지 않으면 GIL의 영향을 받지 않는다.
대표적인 예로 CPU bound인 image processing이 thread-safe한 C extension으로 구성되어 있고 python runtime과 상호작용하지 않는다면, GIL의 영향을 받지 않고 multi-thread로 수행될 수 있다.
하지만 대체적으로 CPU bound 작업과 IO bound작업으로 나누어서 생각할 수 있다.
CPU bound 작업을 할 때 thread를 늘리는 것은 시간을 단축하는데에 전혀 영향을 미치지 않는다.
# single_threaded.py
import time
from threading import Thread
COUNT = 50000000
def countdown(n):
while n>0:
n -= 1
start = time.time()
countdown(COUNT)
end = time.time()
print('Time taken in seconds -', end - start)
출처: <https://timewizhan.tistory.com/entry/Global-Interpreter-Lock-GIL> [Small talks with something]
# multi_threaded.py
import time
from threading import Thread
COUNT = 50000000
def countdown(n):
while n>0:
n -= 1
t1 = Thread(target=countdown, args=(COUNT//2,))
t2 = Thread(target=countdown, args=(COUNT//2,))
start = time.time()
t1.start()
t2.start()
t1.join()
t2.join()
end = time.time()
print('Time taken in seconds -', end - start)
출처: <https://timewizhan.tistory.com/entry/Global-Interpreter-Lock-GIL> [Small talks with something]
따라서 수행하는 작업이 CPU bound job일 때는 thread를 늘려주는 것이 의미가 없다.
하지만 I/O bound job을 사용할 때는 thread를 늘려주는 것이 유리하다.
while a thread is waiting for IO (for you to type something, say, or for something to come in the network) python releases the GIL so other threads can run.
I/O 시점에 GIL이 해제되어 다른 thread가 GIL을 획득하고 작업을 수행하기 때문이다.
Python을 안다면 GIL을 알아야!
python 개발자라면 GIL에 대해서 파악하고 있어야 한다.
그래야 나와 같은 실수를 범하지 않는데, 내가 했던 파워 삽질은 다음편에 소개하도록 하겠다.
오늘은 여기까지!