메인 콘텐츠로 이동하기
  1. Posts/

Python GIL (Global Interpreter Lock)

·441 자

서론 #

파이썬과 다른 프로그래밍 언어의 속도 비교는 항상 문제가 되어 왔으며, 이는 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 Figure

각 스레드는 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이 있음에도 불구하고 멀티 스레드가 더 나은 성능을 낼 수 있습니다.

Reference #

  1. https://ssungkang.tistory.com/entry/python-GIL-Global-interpreter-Lock%EC%9D%80-%EB%AC%B4%EC%97%87%EC%9D%BC%EA%B9%8C