(Python) 전역 해석기 잠금(GIL)

Python Global Interpreter Lock(GIL)이란 무엇입니까?

GIL은 Global Interpreter Lock의 약자로 하나의 쓰레드가 자원을 독점하는 방식으로 실행된다.

멀티스레딩에서는 여러 스레드가 병렬로 작동하지 않습니다.

Python 코드가 실행될 때 여러 스레드 중 하나의 스레드만 실행되도록 허용하는 상호 배제 잠금(뮤텍스)입니다.

한 스레드가 모든 자원을 가지면 다른 스레드를 잠가 다른 스레드가 작동하지 못하게 합니다.


(Python) 전역 해석기 잠금(GIL) 1

GIL을 사용한 이유는 무엇입니까?

Python의 첫 번째 공식 구현인 CPython은 GIL로 Python 객체에 대한 접근을 제한하는 형태로 설계되어 개발 초기에 번거로운 동시성 관리가 편리하고 non-thread-safe CPython의 메모리 관리가 용이합니다.

(스레드세이프가 아니다: 여러 스레드가 동시에 공유 자원에 접근하여 작업을 수행할 수 있는 상태)

Python은 가비지 수집을 통해 메모리 관리를 수행합니다.

간단히 말해서 가비지 수집은 불필요한 메모리를 해제하는 것입니다.

그 방법으로 특정 메모리가 몇 번이나 참조되는지 세어 0이면 해제하는 참조 카운트가 있다.

(아직 자세한 내용은 모르겠고 나중에 공부해보도록 하겠습니다)

여하튼 멀티쓰레딩을 사용하면 메모리 할당 순서가 꼬이고, 같은 객체를 여러 쓰레드에서 잠그고 쓴다면 모든 객체를 잠궈서 참조 횟수를 관리해야 한다.

이러한 비효율성을 방지하기 위해 Python에서 GIL을 사용했습니다.

이는 하나의 잠금을 통해 모든 개체에 대한 참조 횟수의 동기화 문제를 해결합니다.

(스레드는 스택 영역을 제외한 나머지 코드, 데이터 및 힙 영역을 공유합니다.

)

코드로 비교해보자

x=0
def working():
    global x
    for _ in range(1000000):
        x+=1

def working2():
    global x
    for _ in range(1000000):
        x+=1


thread1 = threading.Thread(target=working)
thread2 = threading.Thread(target=working2)

thread1.start()
thread2.start()

thread1.join()
thread2.join()
print(x)

Python은 스레딩 모듈을 제공합니다.

대상은 스레드에 의해 실행될 함수, 스레드를 시작하는 시작 함수 및 스레드가 완료될 때까지 기다리는 조인 함수입니다.

자, 일반적으로 우리가 print(x)할 때, 우리는 2000000을 기대하겠지만, 한 번 나올 것을 출력해 봅시다.


(Python) 전역 해석기 잠금(GIL) 2

오답이 나왔습니다.

그 이유는 위에서 설명한 것처럼 스레드로부터 안전하기 때문입니다.

한 스레드가 x의 값을 변경하면 다른 스레드가 x를 변경하는 프로세스를 무시할 수 있습니다.

이와 같이 여러 스레드가 공유 자원에 동시에 접근할 때 발생하는 문제를 경쟁 조건(race condition)이라고 하며, 이를 해결하기 위해 뮤텍스가 도입되었습니다.


뮤텍스는 하나의 스레드만 공유 리소스에 들어가 작업을 처리할 수 있도록 하는 잠금 개념입니다.

lock = threading.Lock()
x=0
def working():
    lock.acquire()
    global x
    for _ in range(1000000):
        x+=1
    lock.release()

def working2():
    lock.acquire()
    global x
    for _ in range(1000000):
        x+=1
    lock.release()


thread1 = threading.Thread(target=working)
thread2 = threading.Thread(target=working2)

thread1.start()
thread2.start()

thread1.join()
thread2.join()
print(x)


(Python) 전역 해석기 잠금(GIL) 3

정확히 나왔다

이번에는 코드가 하나의 스레드와 두 개의 스레드를 비교합니다.

start = time.time()
i = 0
def thread_working(start,end):
    global i
    for _ in range(start,end):
        i += 1

thread1=threading.Thread(args =(0,1000000), target=thread_working)


thread1.start()

thread1.join()

end = time.time()


print(f"쓰레드 하나만 이용한 경우 소요한 시간 : {end - start}",f"i의값 : {i}")


(Python) 전역 해석기 잠금(GIL) 4

start2 = time.time()

def thread_working(start,end):
    global i
    for _ in range(start,end):
        i += 1

thread1=threading.Thread(args =(0,1000000//2), target=thread_working)
thread2=threading.Thread(args =(1000000//2,1000000), target=thread_working)

thread1.start()
thread2.start()
thread1.join()
thread2.join()
end2 = time.time()

print(f"쓰레드 두개 사용한 경우 소요한 시간 : {end2 - start2}",f"i의값 : {i}")


(Python) 전역 해석기 잠금(GIL) 5

설명에 앞서 args는 함수의 인자를 의미합니다.

흠,,,, 시간의 차이는 없었습니다.

2개라서 그런거 아님? 따라서 4개를 사용해도 큰 차이는 없었습니다.

Python에서 멀티스레딩이 나쁜가요?

  • GIL은 CPU 작업(계산, 이미지 처리 등)에만 적용되는 이야기입니다.

  • 적은 CPU 작업으로 많은 I/O 작업을 수행하는 프로그램에 적합합니다.

    (파일 읽기/쓰기, 네트워크와 상호 작용, 디스플레이와 같은 장치와 통신 등)

  • 스레드는 운영 체제가 프로그램의 이러한 요청에 응답하는 데 소비하는 시간을 격리하므로 차단 I/O를 처리할 때 유용합니다.

이번에 GIL 공부를 하다가 쓰레드도 본 것 같아요!

오늘도 제가 직접 작성한 포스팅을 봐주셔서 감사합니다.

오늘도 좋은 하루 보내시길 바랍니다 🙂