이야기박스

Python. 스레드 이야기 with GIL 본문

Programming Language/Python

Python. 스레드 이야기 with GIL

박스님 2020. 11. 11. 19:03
반응형

# 개요

Python3에서의 GIL(Global Interpreter Lock)이란 어떤 것인지, 그리고 이러한 환경에서 Thread, Process는 어떻게 사용해야 할지 이야기해보도록 하겠습니다.

 

# Process & Thread

[POSIX; 프로세스와 스레드]에 작성된 문서를 바탕으로 정리를 해보았습니다. Python Thread 영역은 CPython과 밀접한 연관이 있고, 프로세스와 스레드의 개념을 잘 설명해주어서 복습한다는 느낌으로 읽어보았습니다.

 

`프로세스`란? 컴퓨터에서 연속적으로 실행되고 있는 프로그램, 즉 작업(Task)들을 일컷는 말.
`스레드`란? 프로세스 내부에 실행되는 작은 작업 단위.  

스레드는 프로세스의 `code`, `data`, `heap` 영역을 공유하고, `stack` 영역을 독립적으로 사용하게 됩니다. 여기서 스레드를 사용할 때는 위 공유 영역에 대한 'Race Condition' 그리고 'Clobbering'을 항상 고려해주어야 합니다.

C언어의 pthreads 라이브러리는 CS에서 이야기하는 프로세스, 스레드 구조를 그대로 구현해두었습니다. 그렇기 때문에, 사용자가 위와 같은 공유자원의 처리를 직접 관리해주어야만 'Thread-Safe'한 구현을 할 수 있습니다.

 

# GIL (Global Interpreter Lock)

GIL은 Python의 thread 구현체인 CPython에 있는 개념입니다. [Python; GlobalInterpreterLock] 공식 문서에서 보면, 다음과 같이 GIL을 설명하고 있습니다.

a mutex that protects access to Python objects, preventing multiple threads from executing Python bytecodes at once

CPython은 C의 pthreads를 참조해 구성했기 때문에, 그대로 사용하려고 하면 Thread-safe 이슈가 발생하게 됩니다.

Note. 구현체별 GIL 존재 여부
- Jython(Java 구현체), IronPython(C# 구현체) 에는 GIL이 없습니다.
- PyPy(Python 자체 구현체)에는 CPython처럼 GIL이 있습니다.
- Cython(CPython 확장)에도 GIL은 있지만, "with" 구문을 사용하면 GIL을 해제할 수 있습니다.

 

## Reference counting for memory management

GIL에서 mutex를 적용하는 방법을 알아보기 전에 먼저 파이썬의 메모리 관리 방식을 알아보겠습니다.

파이썬은 Object를 참조하는 레퍼런스 숫자를 추적하는 방식으로 메모리를 관리하고 있습니다. 

>>> import sys
>>> a = []  # ref 1
>>> b = a   # ref 2
>>> sys.getrefcount(a)  # ref 3
3

예를들어 위와 같은 경우, `a=[]` 선언을 할 때 1개 참조가 생기고 `b=a` 에서 2번째 참조, `sys.getrefcount(a)`에서 한번 더 참조하여 총 3개의 reference count가 생기게 됩니다.

그리고 reference count가 0이 되면, 메모리에서 할당이 해제된다고 보시면 됩니다.

 

## GIL mutex

그렇다면 GIL은 mutex을 어디에 생성하는 걸까요?

 

Python object에 mutex가 있다고 생각을 해보겠습니다. object의 `reference count`을 잠가야 한다면, 모든 object에 대하여 mutex가 필요하게 될 것입니다. 이렇게 많은 mutex를 반복적으로 잠금 및 해제를 하다 보면 성능 저하가 발생하게 됩니다. 또한 Deadlock과 같은 상황이 발생할 수도 있죠.

 

때문에 GIL은 object가 아닌 인터프리터 자체를 잠그게 됩니다. 이렇게 하면 위의 성능저하 및 Deadlock 문제를 회피할 수 있게 되죠. 단.. 이렇게 하면 하나의 스레드만이 Python Code를 실행하게 됩니다.

 

## Why?

GIL은 완벽하지 않아 보입니다. JAVA나 C#과 같이 편리한 인터페이스를 제공하는 다른 언어에 비하면 불친절하다고 느껴지기도 하네요.

 

그렇다면 파이썬 개발자들은 왜 인터프리터에 mutex를 걸게 되었을까요?

[Real Python; python-gil] 문서에 보면 이렇게 적혀 있습니다. 

초창기 파이썬 개발 당시에는 스레드 개념이 미흡했다.
이미 많은 C 확장 라이브러리들이 만들어진 상태에서 스레드 문제를 해결하기 위한 현실적인 솔루션이 GIL이었다.

 

## Single Thread vs Multi Thread Testing

GIL의 동작을 확인할 수 있는 가벼운 테스트 예제입니다.

# single_threaded.py
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
# multi_threaded.py
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

 

싱글 스레드와 멀티 스레드 모드 약 6초정도 소요되었습니다. 심지어 멀티 스레드가 0.7초 정도 더 오래 걸렸네요.

GIL로 인한 락의 획득, 해제가 0.7초 정도 시간이 더 소요되게 한 것 같습니다.

 

그렇다면, 파이썬에서의 멀티 스레드 처리는 효과가 없을까요?

꼭 그렇지는 않습니다. Python에서 I/O 작업을 하게 되는 경우, 잠금이 다른 스레드에 공유될 수 있게 되고 하나의 스레드가 I/O 작업을 하는 동안 다른 스레드에서 작업을 할  수 있게 됩니다.

 

# 요약

  • 파이썬에서 스레드를 사용하게 되는 경우, Interpreter 레벨에서 mutex 잠금이 걸리게 됩니다. 단, 스레드가 I/O 작업에 들어가게 되면, 해당 잠금은 다른 스레드에게 공유될 수 있습니다.
  • CPU 작업이 많은 환경에서 GIL을 피하고 싶다면 Threading이 아닌, Processing 모듈을 사용하는 것을 권장합니다.

 

# 참고

## GIL

https://docs.python.org/3/c-api/init.html#thread-state-and-the-global-interpreter-lock

 

Initialization, Finalization, and Threads — Python 3.9.0 documentation

Initialization, Finalization, and Threads See also Python Initialization Configuration. Global configuration variables Python has variables for the global configuration to control different features and options. By default, these flags are controlled by co

docs.python.org

https://wiki.python.org/moin/GlobalInterpreterLock

 

GlobalInterpreterLock - Python Wiki

In CPython, the global interpreter lock, or GIL, is a mutex that protects access to Python objects, preventing multiple threads from executing Python bytecodes at once. This lock is necessary mainly because CPython's memory management is not thread-safe. (

wiki.python.org

https://realpython.com/python-gil/#why-was-the-gil-chosen-as-the-solution

 

What Is the Python Global Interpreter Lock (GIL)? – Real Python

Python's Global Interpreter Lock or GIL, in simple words, is a mutex (or a lock) that allows only one thread to hold the control of the Python interpreter at any one time. In this article you'll learn how the GIL affects the performance of your Python prog

realpython.com

 

## C; Threading

https://computing.llnl.gov/tutorials/pthreads/

 

POSIX Threads Programming

The third thread waits until the count variable reaches a specified value.      Using Condition Variables Example 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49

computing.llnl.gov

 

## Python; Thread/Process Pool Executor

https://docs.python.org/ko/3/library/concurrent.futures.html

 

concurrent.futures — 병렬 작업 실행하기 — Python 3.9.0 문서

소스 코드: Lib/concurrent/futures/thread.py와 Lib/concurrent/futures/process.py concurrent.futures 모듈은 비동기적으로 콜러블을 실행하는 고수준 인터페이스를 제공합니다. 비동기 실행은 (ThreadPoolExecutor를 사용

docs.python.org

 

반응형