Python GIL (Global Interpreter Lock)
목차
서론 #
파이썬과 다른 프로그래밍 언어의 속도 비교는 항상 문제가 되어 왔으며, 이는 GIL(Global Interpreter Lock) 과 떼려야 뗄 수 없는 관계입니다. 파이썬에서 멀티스레드를 사용하려고 할 때 가장 중요한 키워드 중 하나가 바로 GIL입니다.
sysctl hw.physicalcpu hw.logicalcpu
아래 코드가 실행된 테스트 환경입니다.
hw.physicalcpu: 8
hw.logicalcpu: 8
테스트 코드 #
import random
import threading
import time
# 무작위 생성된 배열에서 최대 숫자 찾기
def working():
max([random.random() for i in range(500000000)])
# 1 싱글 스레드
s_time = time.time()
working()
working()
e_time = time.time()
print(f'{e_time - s_time:.5f}')
# 2 더블 스레드
s_time = time.time()
threads = []
for i in range(2):
threads.append(threading.Thread(target=working))
threads[-1].start()
for t in threads:
t.join()
e_time = time.time()
print(f'{e_time - s_time:.5f}')
결과:
싱글 스레드: 70.46266
더블 스레드: 103.42579
일반적으로 멀티 스레드 사용이 싱글 스레드 실행보다 빠를 것으로 예상하지만, 아이러니하게도 파이썬에서는 더블 스레드의 성능이 싱글 스레드보다 더 나쁩니다. 그 이유는 바로 GIL 때문입니다.
GIL #
글로벌 인터프리터 락은 파이썬 인터프리터가 한 번에 하나의 스레드만 바이트 코드를 실행하도록 하는 락입니다. 모든 리소스를 하나의 스레드에 허용한 다음 잠그고 다른 스레드가 실행되는 것을 방지합니다. 이는 동시 프로그래밍에서 뮤텍스 락과 비슷합니다.
세 개의 스레드로 작업한다고 가정해 보겠습니다. 일반적으로 각 스레드가 병렬로 작업할 것으로 기대하지만, GIL 때문에 그렇지 않습니다. 아래는 파이썬에서 세 개의 스레드가 작동하는 예입니다.
각 스레드는 GIL을 얻고 작업을 수행하며, 다른 모든 스레드는 작업을 중지합니다. 또한 멀티 스레드에서는 컨텍스트 스위칭을 수행하며, 이와 같은 경우에는 싱글 스레드 작업에 비해 시간이 많이 걸립니다.
GIL 사용 이유 #
그렇다면 왜 파이썬은 이렇게 느리게 만드는 GIL을 사용할까요? 그 이유는 참조 카운팅을 훨씬 효율적으로 만들기 때문입니다. 파이썬은 가비지 컬렉션과 참조 카운팅을 사용하여 메모리를 관리합니다.
즉, 파이썬은 모든 객체 및 변수가 얼마나 참조되는지 카운팅합니다. 이 상황에서 여러 스레드가 하나의 변수를 사용하려고 할 때, 모든 변수에 대한 락이 참조 수를 관리하기 위해 필수적입니다. 이를 방지하기 위해 파이썬은 전역적으로 락을 획득하고 해제합니다.
파이썬에서 멀티 스레드가 항상 느리지는 않습니다 #
import random
import threading
import time
def working():
time.sleep(0.1)
max([random.random() for i in range(10000000)])
time.sleep(0.1)
max([random.random() for i in range(10000000)])
time.sleep(0.1)
max([random.random() for i in range(10000000)])
time.sleep(0.1)
max([random.random() for i in range(10000000)])
time.sleep(0.1)
max([random.random() for i in range(10000000)])
time.sleep(0.1)
# 1 Thread
s_time = time.time()
working()
working()
e_time = time.time()
print(f'{e_time - s_time:.5f}')
# 2 Threads
s_time = time.time()
threads = []
for i in range(2):
threads.append(threading.Thread(target=working))
threads[-1].start()
for t in threads:
t.join()
e_time = time.time()
print(f'{e_time - s_time:.5f}')
결과:
싱글 스레드: 6.93310
더블 스레드: 6.05917
이번에는 더블 스레드 작업이 싱글 스레드 작업보다 실제로 더 빠릅니다. 그 이유는 sleep()
함수 때문입니다. 싱글 스레드에서는 sleep
동안 기다려야 하며, 아무것도 할 수 없습니다. 반면에 멀티 스레드 작업에서는 sleep
동안 컨텍스트 스위치가 발생할 수 있습니다.
실제 사례에서는 sleep
대신 스레드가 대기해야 하는 작업(예: I/O 작업)이 있을 때, 파이썬에서도 GIL이 있음에도 불구하고 멀티 스레드가 더 나은 성능을 낼 수 있습니다.