Python의 GIL

hwiba·2022년 9월 23일
0

Python

목록 보기
1/1

표준 Python(CPython)은 인터프리터 언어


GIL를 소개하기전에 먼저 Python을 동작시키는 구현체에대해 알아볼 필요가 있습니다. Python은 구현시키는 구현체에 따라 여러 종류의 Python(CPython, Jython, IronPython, Stackless Python 등)이 있고 이중 우리가 보통 아는 표준 Python은 CPython 구현체가 문장마다 번역하여 코드를 실행시키는 Interpreter Language 입니다.


GIL(Global Interpreter Lock) 이란?


위키피디아에서 GIL이란 말을 인용하자면

A global interpreter lock (GIL) is a mechanism used in computer-language interpreters to synchronize the execution of threads so that only one native thread (per process) can execute at a time. An interpreter that uses GIL always allows exactly one thread to execute at a time, even if run on a multi-core processor.

출처: 위키피디아

GIL은 Python(CPython 실행되는 표준 Python) 인터프리터가 하나의 기본 쓰레드만 실행할 수 있도록 하는 메커니즘입니다. 이는 GIL을 사용하는 멀티 코어 프로세서 에서 실행되더라도 항상 정확히 한 쓰레드가 실행할 수 있도록 허용 하는 것 입니다.

  • 여기서 GIL은 Multi Threading을 불가능하게 해주는 Lock이 아니라 쓰래드를 동시에 병렬로 실행을 불가능 하게하는 Lock입니다.


Execute multi threading in Python


싱글쓰래드와 멀티쓰래딩을 이용한 성능의 차이를 직접 보자.

Execute Code

import time
from threading import Thread


# 500000000번 카운트
def count():
    n = 500000000
    while n > 0:
        n -= 1


# 싱글쓰레드를 이용한 count함수 실행
start = time.time()
count()
count()
end = time.time()
print("end   : ", end - start)

# 멀티쓰레딩을 이용한 count함수 실행
thread_start = time.time()
thread1 = Thread(target=count)
thread2 = Thread(target=count)
thread1.start()
thread2.start()
thread1.join()
thread2.join()
thread_end = time.time()

print("thread end   : ", thread_end - thread_start)

Result

위의 실행결과를 보면 상식적으로 멀티쓰레딩이 더빨라야되는데, 오히려 싱글쓰레딩을 이용한 함수실행이 조금더 빠른것을 볼 수 있는데, 이러한 이유가 Python은 Global Interpreter Lock이 걸려져 있기 때문에 Python코드를 동시에 실행시킬 수 없고, 오히려 병렬로 실행시킴으로써 GIL를 풀고 획득하는 과정이 추가됨으로써 비교적 느린 것을 확인할 수 있습니다.


Why does Python need GIL?


CPython은 메모리관리가 취약

표준 Python의 구현체인 CPython의 메모리관리를 Reference Counting(참조되어진 Python 객체의 갯수를 세는 방식)으로 하기 때문에 동시에 Python코드가 실행되어지면 Race Condition(여러 쓰레드가 공유된 자원을 사용함으로 발생되는 문제)이 발생되어 메모리가 관리가 제대로 되지 않습니다.

Race Condtion 발생예제

  • code

    x = 0  # A shared value
    
    def plus():
        global x
        for i in range(10000000):
            x += 1
    
    def minus():
        global x
        for i in range(10000000):
            x -= 1
    
    t1 = Thread(target=plus)
    t2 = Thread(target=minus)
    t1.start()
    t2.start()
    t1.join()
    t2.join()  # Wait for completionprint(x)
    
    print("x : ", x)
    
  • result

위의 코드에서 멀티쓰레딩을 이용해서 x값을 plus에서는 10000000 더하고 minus에서는 10000000 을 마이너스하여 0이나와야될 것 같은데, 결과값은 다르게 나온 것을 볼 수 있고, 위와같이 공유된자원을 여러쓰레드가 사용함으로써 발생되는 문제인 Race Condition을 확인해 볼 수 있습니다.

Mutex를 이용한 Thread-safe

여기서 Mutex란?

  • 여러 thread가 공유자원에 접근할 때 그 공유자원에 접근하기 위한 메커니즘

그럼 위의 Thread-safe하지 않은 쓰레드들의 공유자원 사용을 동시에 사용할 수 없게 Mutex를 걸어보자

  • Example Code

    from threading import Thread, Lock
     lock = Lock()  # Mutex
      y = 0  # A shared value
    
      def plus():
          global y
          lock.acquire()  # lock 획득
          for i in range(10000000):
              y += 1
          lock.release()  # lock 풀어줌
    
      def minus():
          global y
          lock.acquire()  lock 획득
          for i in range(10000000):
              y -= 1
          lock.release()  # lock 풀어줌
    
          t1 = Thread(target=plus)
          t2 = Thread(target=minus)
          t1.start()
          t2.start()
          t1.join()
          t2.join()  # Wait for completionprint(x)
  • result

위의 Mutex로 이용해서 Race Condition 문제를 해결한 것 처럼 CPython은 메모리관리를 Thread-safe하게 하기위해 GIL을 선택하였습니다.

이외에 Python이 GIL을 선택한 이유로는

  • Sing Thread로도 충분히 빠르다.
  • 병렬 처리의 선택지가 다양(multiprocessing이나 asyncio 등)
  • thread 동시성 완벽 처리를 위해 다른 방법이 존재(Jython, IronPython, Stackless Python 등)

Reference

profile
도전하는 사랑하는 개발자

0개의 댓글