해당 포스팅은 python의 GIL을 이해하고자 아래 post를 참조하여 작성하였다.
GIL을 해결하겠다거나, 성능을 개선하겠다는 내용은 없다.
GIL을 이해하고, threading, multiprocess을 보다 적재적소에 사용하고자 함이다.
많은 내용이 생략됐으므로, 원글을 참조하시길 추천드린다.
https://realpython.com/python-gil/
해당 글을 참조하여 작성됨
GIL은 하나의 스레드만 Python Interpreter에서 제어할수 있도록 하는 mutex이다.
threading library를 사용해서 multi thread로 프로그램을 실행하여도 python interpreter(정확히 말하자면 GIL)에 의해 한번에 하나씩의 thread만 실행이 된다.
python은 메모리관리를 위해 reference counting을 사용한다.
python에서 생성된 객체에는 객체를 가리키는 참조 수를 추적하는 참조 카운트 변수가 있고, 이 카운트가 0에 도달하면 객체가 차지한 메모리는 해제된다.
>>> import sys
>>> a = []
>>> b = a
>>> sys.getrefcount(a)
3
[]는 a변수, 그리고 b, 그리고 getrefcount에 의해 총 3번 참조(getrefcount를 실행한 이후에는 다시 2가 되어있다)되었다.
문제는 이 참조 카운트 변수가 두 스레드가 동시에 값을 늘리거나 줄이는 경쟁 조건으로부터 보호해야 한다는 것이다.
이 참조 카운트 변수는 스레드간에 공유되는 모든 데이터 구조에 잠금을 추가하여 안전하게 유지할 수 있습니다.
Python은 운영 체제에 스레드 개념이 없었던 시절부터 사용되었다. Python은 개발 속도를 높이고 더 많은 개발자가 사용하기 쉽게 설계되었다.
Python에 기능이 필요한 기존 C 라이브러리를 위해 많은 확장이 필요했고, 일관되지 않은 변경을 방지하기 위해 이러한 C 확장에는 GIL이 제공 한 스레드 안전 메모리 관리가 필요했다.
GIL은 구현이 간단하여 Python에 쉽게 추가되었다 하나의 잠금만 관리하면되므로 단일 스레드 프로그램에 대한 성능이 향상되었다
스레드로부터 안전하지 않은 C 라이브러리는 통합하기가 더 쉬워졌고,이러한 C 확장은 Python이 다른 커뮤니티에서 쉽게 채택 된 이유 중 하나가되었다.
따라서, GIL은 CPython 개발자가 Python 초기에 직면했던 문제에 대한 실용적인 해결책이 되었다
간단한 countdown 프로그램으로 GIL을 간략하게 확인해볼 수 있다.
import time
from threading import Thread
from multiprocessing import Process
class Main(object):
def start(self, threads):
COUNT = 50000000
_ts = list()
for _ in range(threads):
_ts.append(Thread(target=self.countdown, args=(COUNT // threads,)))
[t.start() for t in _ts]
[t.join() for t in _ts]
def countdown(self, n):
while n > 0:
n -= 1
위와 같은 코드를 작성해놓고, threads 갯수를 수정하면서 실행속도를 확
if __name__ == '__main__':
s = time.perf_counter()
Main().start(1)
print(f"{time.perf_counter() - s:.10f}")
실행 결과
2.1478449350
2.1695302900
2.2093346020
위 실행결과에서 볼수 있듯이, multi threading으로 실행했지만 실행시간이 줄어들지 않고 되려 늘어나는걸(context 비용에 의해 되려 늘어남) 알수 있다.
이제 위 코드를 multiprocessing을 통해서 GIL의 영향이 받지 않도록 실행해보자
import time
from threading import Thread
from multiprocessing import Process
class Main(object):
def start(self, threads):
COUNT = 50000000
_ts = list()
for _ in range(threads):
_ts.append(Process(target=self.countdown, args=(COUNT // threads,)))
[t.start() for t in _ts]
[t.join() for t in _ts]
def countdown(self, n):
while n > 0:
n -= 1
if __name__ == '__main__':
s = time.perf_counter()
Main().start(4)
print(f"{time.perf_counter() - s:.10f}")
실행결과
0.9452070960
GIL은 이정도로 이해하고 넘어가겠다.