Python은 Global Interpreter Lock(GIL)이라는 큰 특징을 갖고 있는 언어입니다.
Python을 느리게 하는 대표적인 특징이며 본 글은 GIL에 관한 공부 내용입니다.
기본적인 Process와 Thread에 관한 내용
Process
정의
process란 실행중에 있는 프로그램.
또는 운영체제로부터 자원(CPU 시간, 메모리, 주소 공간 등)을 할당받은 작업의 단위(인스턴스).
작동원리
- 프로세스에겐 각각 독립된 메모리 영역(Code, Data, Stack, Heap)을 할당.
1. Code 영역 (정적)
- 프로그램을 실행시키는 실행 파일 내의 명령어 (소스코드).
- Read Only
- 프로세스 종료될 때 까지 유지.
2. Data 영역 (정적)
- 전역변수(global), 정적변수(static), 배열(array), 구조체(structure) 등이 저장.
- 프로세스 종료될 때 까지 유지.
3. Heap 영역 (동적)
- C(malloc, free), C++(new, delete) 등 동적으로 사용하는 영역
4. Stack 영역 (동적)
- 지역변수, 매개변수 등 프로그램이 사용하는 임시 메모리.
- 함수 호출 시 생성. 함수 종료시 반환
프로세스는 가상의 주소와 페이징을 사용한 가상 메모리 공간을 사용
Thread
정의
- 프로세스가 할당받은 자원을 이용하는 실행 흐름의 단위
작동원리
- 쓰레드는 프로세스 내에서 각각 Stack만 따로 할당받고 Code, Data, Heap 영역은 공유한다.
- 한 쓰레드가 프로세스 자원 변경하면, 다른 Sibling Thread 또한 즉시 확인 가능.
- 각각의 스레드는 별도의 레지스터와 스택을 갖고 있지만, 힙 메모리는 서로 읽고 쓸 수 있다.
프로세스 vs 쓰레드
최소 작업 단위
- 프로세스: 운영체제 관점에서 최소 작업 단위
- 쓰레드: CPU 관점에서 최소 작업 단위
: 같은 프로세스 안에 있는 여러 스레드들은 같은 힙 공간을 공유한다. 반면에 프로세스는 다른 프로세스의 메모리에 직접 접근할 수 없다
멀티 프로세스와 멀티 쓰레드
- 멀티 프로세싱: 여러개의 프로세스가 하나의 작업 처리
- 한 프로세스에 문제 발생해도, 다른 프로세스 정상 작동
- Context Switching에서의 오버헤드 (캐쉬 메모리 초기화 등 시간 소모 및 자원 손실 발생)
- IPC(Inter-Process Communication) 사용하여 다른 프로세스 정보 접근 가능 (복잡하고 어렵다.)
* Context Switching
동작 중인 프로세스가 대기를 하면서 해당 프로세스의 상태(Context)를 보관하고, 대기하고 있던 다음 순서의 프로세스가 동작하면서 이전에 보관했던 프로세스의 상태를 복구하는 작업을 말한다.
- 멀티 쓰레딩: 하나의 프로세스가 여러개의 쓰레드를 사용하여 하나의 작업 처리
- 한 쓰레드에 문제 발생할 시 전체에 영향을 줌
- 쓰레드 사이의 작업량이 작아 Context Switching이 빠르다.
- 간단한 통신 방법으로 인한 프로그램 응답 시간 단축(Stack 영역을 제외한 모든 메모리 공유)
- 주의 깊은 설계가 필요하다. (Race Condition, Critical Section 고려하여 설계)
GIL(Global Interpreter Lock)
GIL 이란?
파이썬 위키 정의 (https://wiki.python.org/moin/GlobalInterpreterLock)
'''
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. The GIL prevents race conditions and ensures thread safety. A nice explanation of how the Python GIL helps in these areas can be found here. In short, this mutex is necessary mainly because CPython's memory management is not thread-safe.
'''
먼저 용어들에 대한 개념부터 갖고 가겠습니다.
thread safety
2개의 쓰레드를 갖고 실험을 해봅니다.
하나는 +1을 하는 쓰레드, 하나는 -1을 하는 쓰레드입니다.
같은 변수에 둘 다 N번 실행하면 결과값으로 0이 나오는게 정상적인 상황일 겁니다.
from threading import Thread
x = 0
N = 1000000
def add():
global x
for i in range(N):
x += 1
def subtract():
global x
for i in range(N):
x -= 1
# 스레드 할당
add_thread = Thread(target=add)
subtract_thread = Thread(target=subtract)
# 스레드를 시작한다
add_thread.start()
subtract_thread.start()
# 스레드의 작업이 끝날때까지 대기한다
add_thread.join()
subtract_thread.join()
print(x)
# >> 332573
처음 실행한 값은 332573이 나왔네요.
또 계속 실행할 때 마다 다른 결과값이 나오게 됩니다.
이는 Race Condition 때문입니다.
여러 쓰레드가 공용 자원에 접근하게 될 때 하나의 쓰레드가 선택되게 되며, 이런 상황을 Race Condition이라 부릅니다.
때문에 우리는 스레드들이 Race Condition을 발생시키지 않으며 각자의 일을 잘 수행할 수 있는 Thread Safety 환경을 만들어 주어야 합니다.
# 스레드 할당
add_thread = Thread(target=add)
subtract_thread = Thread(target=subtract)
# add 스레드를 시작하고 끝날때 까지 대기한다
add_thread.start()
add_thread.join()
# subtract 스레드를 시작하고 끝날때 까지 대기한다
subtract_thread.start()
subtract_thread.join()
print(x)
# >> 0
Mutual Exclusion (Mutex)
상호 배제(Mutual Exclusion) 방법 : 공유 객체에 한 스레드만 접근하도록 하여 Thread safety 환경 구성
Mutex를 사용할 때, 스레드에서 공유 객체에 접근하는 부분인 *임계 영역(Critical Section)*을 지정 후 Lock.
Lock --> Lock을 한 스레드만 공유 객체에 접근 가능
from threading import Thread, Lock
x = 0
N = 1000000
mutex = Lock()
def add():
global x
mutex.acquire() # Mutex에 Lock을 걸어 다른 스레드가 접근하지 못하게합니다.
for i in range(N):
x += 1
mutex.release() # Lock을 해제하여 다른 스레드가 접근 할 수 있도록 합니다.
def subtract():
global x
mutex.acquire()
for i in range(N):
x -= 1
mutex.release()
# 스레드 할당
add_thread = Thread(target=add)
subtract_thread = Thread(target=subtract)
# 스레드를 시작한다
add_thread.start()
subtract_thread.start()
# 스레드의 작업이 끝날때까지 대기한다
add_thread.join()
subtract_thread.join()
print(x)
# >> 0
mutex를 통해 처음 코드처럼 스레드마다 코드를 분리하지 않고도 정상적인 값이 나오게 되었습니다.
Referencing Counting
Python의 공식 구현체인 CPython은 객체가 프로그램 내에서 몇 번이나 참조되고 있는지 세는 방법으로 메모리를 관리합니다.
import sys
arr = []
arr_clone = arr
print(sys.getrefcount(arr))
# >> 3
del arr_clone
print(sys.getrefcount(arr))
# >> 2
sys.getrefcount(arr)가 끝난 뒤에 참조 횟수는 하나 더 줄어들게 됩니다.
Python은 각 객체들의 참조 횟수가 0이 되면 가바지 컬렉터가 자동으로 객체를 메모리에서 제거합니다.
Global Interpreter Lock
만약 파이썬이 스레드 동기화를 제대로 이행하지 않는다면 객체의 참조 횟수가 정확한 값이 되지 않으면서 갑자기 객체가 제거되거나, 제거해야 할 객체가 제거되지 않을 수 있습니다.
모든 객체에 mutex를 적용하여 thread safety 환경을 만든다고 해도 데드락(여러개의 스레드가 서로 작업을 끝나기만 기다리는 상태)의 위험이 증가하고, 성능이 단일 스레드보다 더 떨어질 수도 있습니다.
그래서 파이썬은 mutex를 통해 모든 reference 개수를 일일이 보호하지 말고, Python interpreter 자체를 잠그기로 하였습니다. 그리고 그게 바로 GIL 입니다.
보통은 multi thread 보단 multi processing을 사용하는 것 같습니다.
pytorch도 그렇구요.
조금 더 깊이 있는 공부를 하게 되면 추가적으로 업로드 하도록 하겠습니다.
참조
https://byeongjo-kim.tistory.com/29
Process vs Thread 정리
기본부터 탄탄히해보자....!! 본 포스팅은 프로세스와 스레드 부터 멀티 프로세스 멀티 스레드에 대해 간단히 정리된 내용을 담고 있다. 프로세스 - 운영체제로부터 자원(CPU 시간, 메모리, 주소
byeongjo-kim.tistory.com
Python의 Global Interpreter Lock(GIL) | ~/xo.dev
Changhui Lee 데브시스터즈에서 소프트웨어 엔지니어로 일하고 있습니다. 분야에 상관없이 소프트웨어를 개발하는 일을 사랑하며, 일을 제대로 잘하는 것에 관심이 많습니다.
xo.dev
https://dgkim5360.tistory.com/entry/understanding-the-global-interpreter-lock-of-cpython
왜 Python에는 GIL이 있는가
Python 사용자라면 한 번 쯤은 들어봤을 (안 들어봤다 해도 괜찮아요) 악명 높은 GIL (Global Interpreter Lock)에 대해 정리해본다. Global Interpreter Lock 그래서 GIL은 무엇인가? Python Wiki에서는 이렇게..
dgkim5360.tistory.com
'python' 카테고리의 다른 글
Subprocess - flask에 학습 기능 추가 (0) | 2022.01.20 |
---|