program
: 작업을 위해 실행하는 파일process
: 운영체제가 생성하는 작업의 단위(=컴퓨터에서 실행중인 프로그램)thread
: process 안에서 공유되는 메모리를 바탕으로 생성하는 작업의 실행 단위
Thread safe
: 하나의 스레드가 자원에 접근하여 작업을 수행하는 상태race condition(경쟁상태)
: 여러 스레드가 하나의 공유 자원이 동시에 접근하면서 발생하는 문제mutex(상호배제)
: 공유 자원에 하나의 스레드만 진입하며 작업을 처리할 수 있도록 만들어진 lock 개념garbage collection
: 파이썬에서 쓰이는 메모리 관리 기법. 프로그램이 동적으로 할당했던 메모리 영역 중에서 필요없게 된 영역을 해제하는 기능
Reference counting
: 쓰레기 수집(garbage collection)의 한 방식context swich
: 프로세스/스레드 전환. 사용중이던 context(프로세스, 스레드)를 저장하고 다른 context 실행
파이썬에서 GIL란, 파이썬코드가 실행될 때 여러개의 스레드 중 하나의 스레드만 실행될 수 있도록 하는 mutex다. 즉, 하나의 스레드가 모든 자원(리소스)을 차지하고 lock을 걸면, lock을 풀 때 까지 다른 스레드는 실행할 수 없도록 접근을 막는 것이다.
예를 들어, 하나의 함수 sum
을 두 스레드 thread1
, thread2
가 사용하고자 한다.
def sum(x, y):
return x+y
thread1 = sum(1,2)
thread2 = sum(3,4)
한 스레드가 함수를 사용 중일 때는 다른 다른 스레드는 대기 상태에 빠진다. 함수를 실행하는데 1초가 걸린다고 하면 두 스레드가 완료되는 시점은 2초가 지난 뒤일것이다.
이렇듯 GIL 방식은 두 스레드를 전환(context swiching)하는데 시간이 걸리는 데다가 병렬 실행(스레딩 작업)이 제한된다. 파이썬이 다른 언어보다 속도가 느린 이유다.
GIL는 스레드를 직렬로 실행시키면서 멀티스레딩의 장점을 없애버린다. 그럼 왜 파이썬은 GIL를 사용할까? 그 이유는 파이썬의 thread safe
하지 않은 garbage collection
메모리 관리 정책을 사용하기 때문이다. 동적으로 메모리를 할당한 뒤, 그 메모리 공간이 필요가 없어지면 할당을 해지하는 관리 방법인데 이는 다중스레드가 발생하면 메모리 할당 순서가 꼬여버린다.
이게 무슨말인가 하면, 아래의 예시를 보자.
x
라는 변수에 1씩 100만번을 더하는 함수를 만들고, 이를 실행하는 두 스레드를 만들었다.
import threading
x = 0
def foo():
global x
for _ in range(1000000):
x += 1
thread1 = threading.Thread(target=foo)
thread2 = threading.Thread(target=foo)
thread1.start()
thread2.start()
thread1.join()
thread2.join()
print(x)
print("time :", time.time() - start)
이 코드를 돌려보면 이런 결과값이 나온다.
100만을 더하는 함수를 두 번 돌렸으니 결과값이 200만이 나와야 하는데 그보다 못한 수가 나온다. 그리고 코드를 실행할 때마다 결과값이 다르다. 두 연산식이 꼬여버린(race condition) 것이다. 위 함수에 lock을 걸어서 한 스레드의 작업이 끝날 때까지 다른 스레드가 접근할 수 없도록 하고 다시 실행을 해보자.
import threading
import time
start = time.time()
LOCK = threading.Lock()
x = 0
def foo():
LOCK.acquire()
global x
for _ in range(1000000):
x += 1
LOCK.release()
thread1 = threading.Thread(target=foo)
thread2 = threading.Thread(target=foo)
thread1.start()
thread2.start()
thread1.join()
thread2.join()
print(x)
print("time :", time.time() - start)
이제야 200만이 나온다. 심지어 속도도 조금 더 빨라졌다!
GIL이 적용되면 직렬 실행이 안되니 멀티 스레드는 무조건 느릴거라고 생각할 수 있다. 하지만 위에서 든 예시를 보면 알겠지만 그렇지만도 않다.
GIL은 cpu 동작에서 적용이 되고, 스레드가 I/O 작업을 실행하는 동안에는 다른 스레드가 cpu 동작을 동시에 실행할 수 있다. 따라서 cpu 동작이 많지 않고 I/O동작이 더 많은 프로그램에서는 멀티 스레드의 효과를 얻을 수 있다.
- cpu 작업
- 곱셈등의 연산, 이미지 처리 등
- I/O 제외한 연산
- I/O 작업
- 파일 읽기/쓰기, 네트워크 통신 등
- 멀티스레드가 발생해도 GIL가 없어서 속도가 빠르다
[참고사이트]
Python 공식문서 - GlobalInterpreterLock
Python GIL(Global Interpreter Lock)
python gil이 있으니까 thread safe 할까요?
[Python] 파이썬 멀티 쓰레드(thread)와 멀티 프로세스(process)
Python Global Interpreter Lock Tutorial