파이썬

파이썬 Global Interpreter Lock (GIL) - 1

cocojen 2022. 5. 1. 23:42

GIL

- GIL의 필요성

GIL의 필요성을 이해하려면 우선 스레드에 대해서 이해하고 있어야합니다.

스레드가 가지고 있는 어떤 특성 때문에 어떤 현상이 발생하고, 이 현상을 방지하기 위해 GIL이 도입되었기 때문입니다.

 

간단한 비유를 들어서 설명해보겠습니다.

위의 상황처럼 열 명의 사람들이 눈치게임을 하고 있습니다.

눈치게임은 말 그대로 눈치를 보다가 동시에 같은 숫자를 외치는 사람이 있으면 그 둘이 걸리는 게임이죠.

원래 눈치게임은 게임을 끝내야 하니까 끝까지 동시에 번호를 외치는 사람이 없으면 번호를 안 외친 마지막 사람이 걸리는데

이런 룰이 없이, 10명이 아무도 겹치지 않고 끝까지 번호를 각각 외치면 되는 게임이라고 생각해봅시다!

이 게임이 무리없이 잘 돌아갈까요?

번호를 외칠 순서도 정해져있지 않고, 누가 어떤 번호를 부를지 모르는 상황에서,

위의 짤처럼 두 명이 동시에 1 을 외칠 수도 있고,  1은 운 좋게 피해가도, 2,3,4 .. 10 까지.. 각자 따로 번호를 부르기 쉽지 않을 것입니다.

 

여기서 1부터 10까지의 숫자가 변수(공유자원에 있는)이고, 사람들 하나하나가 쓰레드라고 생각해 볼까요?

위의 짤은 두 쓰레드가 변수x(여기서는 숫자 1이라고 생각)에 동시 접근한 경우라고 볼 수 있습니다.

여기서는 왼쪽 사람이 아주 조금 더 빨리 앉는데요, 그러면 이 왼쪽 사람이 변수 x에 먼저 접근한 것이죠.

다른 말로 하면 왼쪽 사람이 변수 x에 먼저 접근해서 이미 다 사용해버렸어요.

그 후 오른쪽 사람이 앉는데, 오른쪽 사람이 x에 접근해도 x는 이미 왼쪽 사람이 사용했기 때문에 더이상 사용할 수 없는 상태입니다.

이 경우 에러가 발생하면서 게임이 끝나게 됩니다. 

 

그런데 만약 마이크를 쥔 사람만 숫자를 외칠 수 있다는 제한을 걸어놓으면 어떨까요?

그런 장치가 있다면 사람들이 동시에 같은 번호를 외치는 것을 방지할 수 있겠죠. 

이 역할을 하는 것이 바로 GIL 입니다.

 

- GIL 의 역할

GIL은 오직 하나의 thread가 파이썬 interpreter의 제어권을 갖도록 하는 lock입니다.

thread가 파이썬 bytecode를 실행하기 위해서는 GIL을 획득해야 하고, GIL을 반납할 때까지 다른 thread는 파이썬 bytecode를 실행할 수 없습니다.

그렇기 때문에 thread가 여러 개 있어도 한 시점에는 오직 하나의 thread만이 실행될 수 있습니다

 

왜 한 번에 하나의 쓰레드만 파이썬 인터프레터의 제어권을 가져야할까요? 

GIL 이 없을 경우에, 멀티 스레딩 구조에서 다음과 같은 일이 생길 수 있습니다.

 

class User(object):
    def __del__(self):
        print("No reference left for {}".format(self))

user1 = User()
user2 = user1
user3 = user1

>>> user1 = None
>>> user2 = None
>>> user3 = None
No reference left for <__main__.User object at 0x212bee9d9>

(위 코드 예시는 https://www.journaldev.com/17927/python-garbage-collection-gc 에서 참고한 것임)

마지막 코드를 보면 더 이상 User 오브젝트에 대한 레퍼런스가 남아있지 않다고 하며 에러가 반환되는 것을 볼 수 있습니다.

user3 까지 None에 assign 한 이후, 이 객체는 garbage collected 되고, garbage collecter는 __del__ 함수를 호출합니다.

 

The __del__() method is a known as a destructor method in Python. It is called when all references to the object have been deleted i.e when an object is garbage collected. Note : A reference to objects is also deleted when the object goes out of reference or when the program ends.

 

- Garbage Collector 와 Reference Counting

위와 같은 일이 생기는 이유는 파이썬 가비지콜렉터는 메모리 해제를 할 때, Reference Counting 을 사용하기 때문입니다.

파이썬은 객체가 얼마나 참조되고 있는지를 세서, 즉 레퍼런스를 카운팅해서 이 값이 0이 되면 더이상 불필요하다고 판단하고,

이 불필요한 것을 알아서 소멸시켜줍니다.

 

스레드는 스택을 제외한 나머지 메모리를 공유한다

 

그러면 멀티쓰레딩에서는 이 레퍼런스 콜렉팅이 정확하지 않을 수 있게 됩니다.

예를 들어, 멀티쓰레딩에서 먼저 실행된 스레드1이 레퍼런스 카운팅을 시작합니다.

스레드1의 작업이 끝난 후에 변수 x는 레퍼런스 카운팅이 0이 되어서 쓸모가 없어지고, 쓰레드1의 가비지 콜렉터가 이 변수를 지워버렸습니다. 

그런데 마침 스레드1과 같은 자원을 공유하고 있었던 스레드2가 뒤늦게 이 x변수에 접근하려고 합니다. 

이미 변수 x를 지우고 난 후에 말이죠. 이 경우에 프로그램은 에러를 발생시키겠죠?

파이썬에서는 이 문제를 예방하기 위해서 global interpreter lock 을 걸어놓았습니다.

 

 

 

 

요 그림을 보면 알 수 있는데요, 스레드가 동시에 실행되는 경우는 없고, 한 스레드가 작업을 하는 동안 GIL이 걸립니다.

파이썬은 싱글 스레드 라는 말이 여기서 나옵니다. (싱글 쓰레드에서 순차적으로 동작, GIL때문에 한 번에 하나의 작업 밖에 못 함)

파이썬은 이렇게 작동하기 때문에 발생하는 장단점이 있는데요,

GIL의 장단점에 대해서는 다음 포스트에 계속해서 정리하겠습니다.