Python의 GIL(Global Interpreter Lock)이란?

Garam·2024년 9월 22일

What Is the Python Global Interpreter Lock (GIL)? - by Abhinav Ajitsaria

Global Interpreter Lock

GIL은 파이썬 인터프리터에 대한 컨트롤을 오직 한 스레드에만 허락하는 Mutex (or lock)를 의미한다.

이는 같은 순간에 실행될 수 있는 스레드가 오직 하나뿐임을 의미한다. 이는 CPU-bound한 작업과 멀티 스레드 작업을 해야할 때 난관으로 다가올 수 있다.


파이썬에서 GIL이 사용되는 이유

파이썬은 메모리 관리에 reference counting을 사용한다. 이는 파이썬의 객체가 해당 객체를 참조하는 횟수를 추적하는 reference count 변수가 있다는 뜻이다. 이 count가 0에 다다르면 객체가 점유하고 있던 메모리가 해제된다.

>>> import sys
>>> a = []
>>> b = a
>>> sys.getrefcount(a)
3

위의 예제에서 []의 reference count는 3으로 집계된다. a, b, sys.getrefcount() 총 세군데에서 참조되었기 때문이다.

이 reference count 변수가 만약 두개의 스레드로부터 동시에 접근된다면 메모리가 해제되지 않아 누수되거나, 혹은 참조하고 있는 객체가 여전히 존재하는데도 메모리를 해제해버리는 불상사가 일어날 수 있다.

이러한 현상을 방지하기 위해서는 모든 스레드가 공유하는 자료 구조에 전부 lock을 걸어 비일관적으로 수정되는 것을 방지할 수 있다. 그러나 그렇다고 해서 모든 객체에 lock을 거는것은 Deadlock이라는 또 다른 문제를 야기할 수 있다. 또 반복적으로 lock을 걸고 해제하는 과정에서 성능이 저하된다.

GIL은 인터프리터 자체에 대한 단일 잠금 장치로, 모든 파이썬 바이트코드를 실행하기 위해 인터프리터 lock을 획득해야 한다는 규칙을 의미한다. 잠금 장치가 하나뿐이기 때문에 교착 상태가 방지되고, 성능 오버헤드가 발생하지 않는다. 하지만 이로 인해 CPU-bound한 파이썬 프로그램은 사실상 단일 스레드로 실행된다.

GIL은 루비 같은 다른 언어의 인터프리터에서도 사용되지만, 이는 이 문제를 해결하는 유일한 방법은 아니다. 일부 언어는 reference counting 대신 가비지 컬렉션과 같은 방법을 사용하여 thread-safe한 메모리 관리를 구현한다.

반면, 이러한 언어들은 GIL이 제공하는 단일 스레드 성능의 이점을 잃게 되므로, JIT 컴파일러 같은 성능 향상 기능을 추가해 이를 보완해야한다.


GIL의 동작 방식


출처: https://www.datacamp.com/tutorial/python-global-interpreter-lock

  • GIL 획득: 스레드가 Python 객체를 수정하거나 읽기 위해 GIL을 획득한다. GIL을 획득한 스레드는 코드 실행을 시작할 수 있다.
  • GIL 해제: 스레드가 GIL을 해제하면, 다른 스레드가 GIL을 획득하여 실행한다. 이 전환 과정은 오버헤드를 발생시킨다.

GIL이 선택된 이유

파이썬은 OS에 스레드 개념이 없던 시절에 설계되었으며, 간편한 사용과 빠른 개발을 위해 설계된 언어다.

많은 Extension들은 기존의 C 라이브러리를 파이썬에 이식하기 위해 만들어졌다. 일관된 변경을 위해, C Extension들은 GIL이 제공한 thread-safe한 메모리 관리를 필요로 했다.

GIL은 파이썬에 쉽게 구현될 수 있었고, 단일 스레드 프로그램의 성능을 향상시켰다. 이에 따라 thread-safe 하지 않은 C 라이브러리와의 통합이 쉬워졌다. GIL은 CPython 개발자들이 파이썬의 초창기에 직면한 문제를 해결해주었다.


GIL이 Multi-thread 파이썬 프로그램에 미치는 영향

먼저 CPU-bound한 태스크에서 Multi Thread를 적용했을 시 어떤 차이가 있는지 살펴보자.


  • 싱글 스레드로 countdown 함수 구동
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)
$ python single_threaded.py
Time taken in seconds - 6.20024037361145

  • 두 개의 스레드로 countdown 구동
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)
$ python multi_threaded.py
Time taken in seconds - 6.924342632293701

두 버전의 코드는 실행시간의 차이가 거의 없다. GIL이 CPU-bound한 스레드의 병렬 실행을 막았기 때문이다.

GIL은 I/O-bound한 프로그램에는 큰 영향을 주지 않는다. 입출력을 위한 긴 대기시간 동안 lock이 스레드간에 공유가 되기 때문이다. 즉, 스레드가 I/O 작업을 수행하기 위해 대기하는 동안 GIL이 다른 스레드에 의해 해제될 수 있으며 하나의 스레드가 데이터를 읽거나 쓰기 위해 대기하는 동안 다른 스레드가 CPU를 사용할 수 있게 된다는 것이다.

그러나 프로그램이 CPU-bound할 때는 여러 스레드가 동시에 CPU를 사용할 수 없기 때문에 GIL에 의해 단일 스레드처럼 동작하게 된다.

또 이 경우 실행 시간이 오히려 불필요하게 증가한다. 단일 스레드로 작성된 경우와 비교했을 때 GIL을 획득하고 해제하는 과정에서 오버헤드가 발생하기 때문이다.


GIL 문제를 해결하는 방법

GIL 때문에 어려움을 겪고 있다면 다음과 같은 선택을 고려해볼 수 있다.

1. MultiProcessing

가장 일반적인 방법은 멀티 프로세싱을 사용하는 것이다. 여러 개의 프로세스를 생성한 후 각각의 파이썬 인터프리터와 메모리 공간을 갖게 하여 GIL의 영향을 피할 수 있다. 이 방식은 CPU-바운드 작업에서 더 나은 성능을 제공할 수 있다.

from multiprocessing import Pool
import time

COUNT = 50000000
def countdown(n):
    while n>0:
        n -= 1

if __name__ == '__main__':
    pool = Pool(processes=2)
    start = time.time()
    r1 = pool.apply_async(countdown, [COUNT//2])
    r2 = pool.apply_async(countdown, [COUNT//2])
    pool.close()
    pool.join()
    end = time.time()
    print('Time taken in seconds -', end - start)
$ python multiprocess.py
Time taken in seconds - 4.060242414474487

멀티 스레드로 작성했던 코드보다 효율이 증가한 것을 확인할 수 있다.

참고로 실행 시간이 기대처럼 반으로 줄어들지 않은것은 프로세스 관리 자체의 오버헤드 때문이다. 멀티 프로세스는 멀티 스레드보다 더 무거운 작업이므로 작업시에 이 부분을 고려해야 한다.


2. 다른 Interpreter 선택

파이썬의 인터프리터에는 여러 종류가 있다. 인지도가 있는 인터프리터는 CPython, Jython, IronPython, PyPy 등이 있다. GIL은 CPython, 초기 파이썬에만 존재한다. 만약 프로그램에 다른 인터프리터를 적용할 수 있다면 고려해보는 것도 좋은 방향이다. (그러나 일반적으로는 권장되지 않는다고 함)



0개의 댓글