[IT 심층 분석]
Python GIL(전역 인터프리터 잠금)이 멀티코어 CPU 성능을 죽이는 원리와 진짜 병렬 처리 탈출구
김민준 · IT 시스템 엔지니어|
데이터 파이프라인 팀에서 새로 합류한 동료가 CPU 코어가 64개나 되는 막강한 서버에서 파이썬 멀티스레딩 코드를 돌렸는데 단일 코어로 실행할 때와 처리 속도가 거의 동일하다는 황당한 성능 결과를 가져왔습니다. 서버의 CPU 점유율 대시보드를 보면 단 1~2개의 코어만 분주하게 작동하고 나머지 62개는 완전히 무용지물로 방치되어 있었습니다. 이 당황스러운 현상의 장본인은 파이썬 언어 자체에 내재된 구조적 제약인 GIL(Global Interpreter Lock)이었습니다.
CPython 인터프리터는 내부적으로 참조 카운터를 통해 메모리를 관리하는데 여러 스레드가 동시에 이 카운터를 수정하면 경쟁 조건이 발생할 수 있습니다. 이를 막기 위해 초창기 설계자들은 인터프리터 전체에 하나의 거대한 잠금을 걸어두는 방식을 택했고 이것이 GIL입니다. GIL이 존재하는 한 파이썬 인터프리터는 아무리 스레드가 많더라도 어느 순간에나 단 하나의 스레드만이 바이트코드를 실행할 수 있습니다. 따라서 CPU 연산이 많은 작업을 스레드로 병렬화하려는 시도는 GIL에 의해 번번이 차단당할 수밖에 없는 구조적 한계를 안고 있습니다.
대안으로 선택한 경로는 두 가지였습니다. 첫 번째는 멀티프로세싱 모듈의 하위 개념인 multiprocessing.Pool을 이용하는 방법입니다. 프로세스는 스레드와 달리 각자 독립된 인터프리터와 GIL을 갖기 때문에 여러 프로세스를 동시에 실행하면 진정한 병렬 CPU 작업이 가능합니다. 실제로 파이프라인 코드를 멀티프로세싱 방식으로 재작성하자 코어 수에 비례하는 거의 선형적인 성능 향상을 얻을 수 있었습니다. 두 번째는 연산 집약적인 코어 모듈들을 Cython이나 ctypes를 통해 C 확장으로 내려보내는 방법입니다. C 코드를 실행하는 시간 동안은 GIL을 해제하도록 명시적으로 선언할 수 있으며 이를 통해 순수 파이썬 코드보다 훨씬 자유롭게 병렬 실행이 가능해집니다. 두 방법을 결합하여 I/O 대기 구간에서는 asyncio로 비동기 처리를 하고 CPU 연산 구간에서는 프로세스 풀로 분산 처리하는 하이브리드 아키텍처를 설계하자 총 파이프라인 처리 시간이 기존 단일 스레드 대비 약 40배 단축되는 극적인 성능 향상을 이끌어낼 수 있었습니다.