파이썬의 전역 인터프리터 잠금(GIL) 이해하기
Wenhao Wang
Dev Intern · Leapcell

파이썬에서의 동시성 잠금 해제
가독성과 방대한 생태계로 찬사를 받는 파이썬은 진정한 병렬 실행과 관련하여 종종 조사를 받습니다. 이 대화는 필연적으로 전역 인터프리터 잠금, 즉 GIL이라는 개념으로 이어집니다. 파이썬에서 고성능 동시 애플리케이션을 작성하려는 개발자에게 GIL을 이해하는 것은 학문적인 것 이상으로 효과적인 솔루션을 설계하는 데 매우 중요합니다. 이 기사에서는 GIL이 무엇인지, 왜 도입되었는지, 그리고 파이썬 개발자가 GIL의 존재를 어떻게 탐색할 수 있는지 명확히 밝힐 것입니다.
동시 실행의 역설
GIL 자체를 자세히 살펴보기 전에 몇 가지 기본 개념을 확립해 보겠습니다.
- 동시성과 병렬성: 이 용어들은 종종 상호 교환적으로 사용되지만 구별됩니다. 동시성은 겉보기에 동시에 여러 작업을 처리하는 것(예: 단일 CPU 코어를 사용하여 작업 간에 빠르게 전환)을 다룹니다. 병렬성은 문자 그대로 동시에 여러 작업을 실행하는 것(예: 여러 CPU 코어를 사용하여 다른 작업을 동시에 실행)을 다룹니다. GIL이 있는 파이썬은 동시성에는 뛰어나지만 일반적으로 단일 프로세스 내에서 스레드를 사용하는 진정한 병렬성에는 어려움을 겪습니다.
- 스레드: 스레드는 단일 프로세스 내에서 가벼운 실행 단위입니다. 스레드는 프로세스와 동일한 메모리 공간을 공유하므로 스레드 간의 통신이 비교적 쉽습니다. 이상적인 멀티스레드 환경에서는 여러 스레드가 여러 CPU 코어에서 병렬로 실행될 수 있습니다.
- 프로세스: 프로세스는 각자 고유한 메모리 공간을 가진 독립적인 실행 단위입니다. 프로세스 간의 통신은 더 복잡합니다(예: 프로세스 간 통신 메커니즘 사용). 여러 프로세스는 항상 다른 CPU 코어에서 병렬로 실행될 수 있습니다.
이제 GIL에 주의를 기울여 보겠습니다.
GIL이란 무엇인가?
전역 인터프리터 잠금은 파이썬 객체에 대한 액세스를 보호하는 뮤텍스(상호 배제 잠금)로, 여러 네이티브 스레드가 동시에 파이썬 바이트코드를 실행하는 것을 방지합니다. 이는 멀티코어 프로세서에서도 한 번에 하나의 스레드만 파이썬 바이트코드를 실행할 수 있음을 의미합니다. GIL은 파이썬 자체의 기능이 아니라 가장 일반적인 파이썬 인터프리터인 CPython의 구현 세부 사항입니다.
GIL은 왜 존재하는가?
GIL의 존재는 파이썬의 초기 역사에서 주로 메모리 관리 및 C 수준 데이터 구조에 대한 스레드 안전 액세스를 단순화하기 위한 근본적인 설계 선택에 뿌리를 두고 있습니다.
- 간소화된 메모리 관리: 파이썬은 가비지 수집을 위해 참조 개수를 사용합니다. 모든 파이썬 객체에는 참조 개수가 있으며, 0이 되면 객체의 메모리가 해제됩니다. GIL이 없으면 여러 스레드가 동시에 참조 개수를 증감시켜 경쟁 상태, 메모리 누수 또는 충돌을 유발할 수 있습니다. GIL은 한 번에 하나의 스레드만 참조 개수를 수정할 수 있도록 보장하여 가비지 수집 메커니즘을 더 간단하고 강력하게 만듭니다.
- C 확장과의 통합: NumPy, SciPy와 같은 많은 강력한 파이썬 라이브러리는 C로 작성됩니다. GIL은 이러한 C 확장을 복잡한 스레드 안전 잠금 메커니즘을 신경 쓸 필요 없이 쉽게 통합할 수 있도록 합니다. C 코드는 한 번에 하나의 스레드만 파이썬 객체와 상호 작용하고 있다고 가정할 수 있습니다.
- 역사적 맥락: 파이썬이 처음 설계되었을 때는 멀티코어 프로세서가 일반적이지 않았습니다. GIL이 제공하는 구현의 단순성과 안전성은 병렬성에 대한 성능 의미를 능가했습니다.
GIL은 성능에 어떤 영향을 미치는가?
GIL의 영향은 "CPU 바운드" 작업, 즉 외부 리소스(I/O 등)를 기다리는 대신 대부분의 시간을 계산 수행에 소비하는 작업에서 가장 두드러집니다. 예를 들어 복잡한 수학 계산, 이미지 처리 또는 데이터 변환은 CPU 바운드입니다. CPU 바운드 작업을 수행하는 두 개의 스레드가 있는 경우 GIL은 스레드가 차례를 기다리도록 강제하여 실행을 효과적으로 직렬화하고 진정한 병렬 속도 향상을 방지합니다.
반대로, "I/O 바운드" 작업, 즉 입력/출력 작업(네트워크 요청, 파일 읽기/쓰기, 데이터베이스 쿼리 등)을 기다리는 데 대부분의 시간을 소비하는 작업은 GIL의 영향을 덜 받습니다. 한 스레드가 I/O 작업 완료를 기다리는 동안 GIL이 해제되어 다른 스레드가 파이썬 바이트코드를 실행할 수 있습니다. 이것이 멀티스레딩이 파이썬에서 I/O 바운드 애플리케이션에 여전히 성능 이점을 제공할 수 있는 이유입니다.
GIL 우회
GIL은 단일 프로세스 내 스레드 간의 파이썬 바이트코드 병렬 실행을 방지하지만, 이 제한 사항을 극복하기 위한 몇 가지 효과적인 전략이 있습니다.
-
멀티프로세싱: 파이썬에서 진정한 병렬성을 달성하는 가장 일반적이고 강력한 방법입니다.
multiprocessing
모듈을 사용하면 각자 자체 파이썬 인터프리터와 메모리 공간을 가진 여러 프로세스를 시작할 수 있습니다. 각 프로세스에는 자체 GIL이 있으므로 여러 CPU 코어에서 파이썬 바이트코드를 동시에 실행할 수 있습니다.import multiprocessing import time def cpu_bound_task(n): result = 0 for i in range(n): result += i*i return result if __name__ == '__main__': start_time = time.time() # 멀티프로세싱 사용 pool = multiprocessing.Pool(processes=2) # 2개의 CPU 코어 사용 results = [pool.apply_async(cpu_bound_task, args=(10**7,)) for _ in range(2)] output = [r.get() for r in results] pool.close() pool.join() print(f