관리 메뉴

밤 늦게까지 여는 카페

[Python] GIL로 인한 멀티쓰레드 성능 이슈 분석 - 멀티쓰레드가 멀티쓰레드가 아니에요 ㅜ 본문

For Fun/잡학 지식

[Python] GIL로 인한 멀티쓰레드 성능 이슈 분석 - 멀티쓰레드가 멀티쓰레드가 아니에요 ㅜ

Jㅐ둥이 2024. 4. 18. 02:19
반응형

안녕하세요. 날씨가 따뜻해졌는데 다들 봄을 잘 즐기고 있으신가요?

저는 낮에 기온이 20도가 넘어가니 땀이 나더라고요 ㄷㄷㄷ 올 여름은 시원했으면 좋겠습니다 ㅜㅠ

 

이번에는 python의 Global Interpreter Lock(GIL)에 대해서 조금 공부해보려고 합니다.

 

갑자기 python GIL을 공부하는 이유가 무엇이냐고요?

최근에 AWS IoT Core 관련 작업을 진행하면서 프로그램이 멈추는 버그를 겪었는데 그 원인을 GIL로 추정하고 있기 때문입니다...

 

GIL이 무엇인지, GIL로 인해서 겪을 수 있는 성능 이슈에 어떤 것이 있는지 알아보도록 하겠습니다.


1. GIL이 무엇인가요?

GIL은 파이썬 인터프리터가 한 번에 하나의 스레드만 실행하도록 제한하는 메커니즘입니다.

 

파이썬의 핵심 구성 요소인 CPython에서 멀티스레딩 시 메모리 관리 문제를 방지하기 위해 도입되었습니다.

 

이로 인해, CPU 바운드 작업에서는 멀티스레딩을 사용하더라도 기대했던 성능 향상을 얻기 어렵게 됩니다.

 

2. "멀티스레딩 시 메모리 관리 문제" 부분을 조금 더 자세히 설명해주실 수 있을까요?

malloc 함수와 free 함수를 이용해서 메모리의 할당과 해제를 개발자가 직접 관리하는 C언어와는 다르게 파이썬은 프로그래밍 언어가 메모리를 관리해줍니다.

 

그러기 위해서는 가비지 컬렉션으로 메모리들이 관리되고 있겠죠? 파이썬은 참조 횟수 계산 방식으로 메모리를 관리하고 있다고 합니다.

 

그렇다면 여러 개의 스레드가 실행되고 있을 때 각 메모리의 참조 횟수를 정확히 관리하기 위해서는 어떻게 해야 할까요?

  • 만약 참조 횟수를 정확히 관리하지 못한다면 사용하고 있는 메모리가 해제되거나, 사용되지 않는 메모리가 계속 남게 되어 심각한 에러를 발생시킬 수 있습니다 ㄷㄷ

가장 쉽게 생각할 수 있는 방법이 아마 Lock을 사용하는 것일 거에요. 하지만 Lock을 사용해서 참조 횟수를 관리하는 방법들은 모두 데드락을 발생시키게 됩니다.

 

파이썬의 창시자인 귀도 반 로섬(Guido van Rossum)이 제안한 해결책이 바로 GIL이었습니다!

위의 내용은 파이썬의 핵심 기여자인 Larry Hastings가 파이콘2015에서 발표한 내용을 참고했습니다.

- 참고 링크: PyCon 2015 - Python's Infamous GIL by Larry Hastings

 

3. 그러면 GIL 도입은 단점만 있는 거 아닌가요?

GIL 도입이 단점만 있던 것은 아니라고 하더라고요.

 

1) 단순하고, 2) 데드락을 발생시키지 않으며, 3) I/O 바운드 작업에서는 괜찮으면서

 

thread-safe 하지 않은 C 라이브러리들도 파이썬과 통합되니 파이썬의 인기가 높아지게 되었다고 합니다!

 

4. 혹시 CPU 바운드 작업에서 멀티스레딩을 사용할 때 성능이 개선되지 않는 예시 있을까요?

저도 예시를 보고 싶어서 다음과 같이 실험 코드를 작성했습니다.

더보기
import time
import random
from threading import Thread

def cpu_bound_task(numbers):
    return [x*x for x in numbers]

def single_thread(numbers):
    start_time = time.time()
    cpu_bound_task(numbers)
    end_time = time.time()
    print(f"싱글 스레드 처리 시간: {end_time - start_time}초")

def multi_thread(numbers, num_of_thread):
    threads = []
    split_numbers = [numbers[i::num_of_thread] for i in range(num_of_thread)]  # 숫자 리스트를 4개로 분할
    start_time = time.time()
    for i in range(num_of_thread):
        thread = Thread(target=cpu_bound_task, args=(split_numbers[i],))
        thread.start()
        threads.append(thread)
    for thread in threads:
        thread.join()
    end_time = time.time()
    print(f"멀티 스레드({num_of_thread}개) 처리 시간: {end_time - start_time}초")

numbers1 = [random.randint(1, 10000000) for _ in range(10000000)]  # 1부터 10,000,000까지의 숫자
numbers2 = [random.randint(1, 10000000) for _ in range(10000000)]  # 1부터 10,000,000까지의 숫자
numbers3 = [random.randint(1, 10000000) for _ in range(10000000)]  # 1부터 10,000,000까지의 숫자
numbers4 = [random.randint(1, 10000000) for _ in range(10000000)]  # 1부터 10,000,000까지의 숫자

single_thread(numbers1)
multi_thread(numbers2, 4)
multi_thread(numbers3, 16)
multi_thread(numbers4, 64)

실험 결과는 예상하는대로 CPU 바운드 작업에서는 멀티스레드를 사용하더라도 성능이 좋아지지 않았습니다.

GIL로 인해서 CPU 바운드 작업은 멀티스레드를 사용했을 때 오히려 성능이 안 좋아진다.

 

오히려 스레드 개수가 많아질수록  컨텍스트 스위칭에 시간이 걸려서 처리 시간이 더 길어진 것으로 보입니다.

 

5. 그럼 파이썬은 CPU 바운드 작업의 성능을 향상시킬 방법이 없는 걸까요?

멀티프로세싱 모듈을 사용하는 방법이 있습니다!

 

하지만 멀티프로세싱 모듈을 사용하면 프로세스 간 통신을 고려해야 합니다.


이렇게 파이썬 GIL에 대해서 알아보니 억까 당하는 것 같아서 속상했던 제 마음도 한결 누그러워지네요.

 

요즘 파이썬으로 작업을 많이 하고 있는데 파이썬 관련한 경험들을 또 기록하도록 하겠습니다 ㅎㅎ

반응형