GIL에 대한 내용은 두 번째 단락(GIL) 부터입니다.

Intro

멀티스레딩을 해야한다.

프로젝트 도중 직렬로 처리되고 있는 작업을 병렬처리로 바꿔야 하는 상황이 있었습니다.

먼저, 해당 작업의 기본적인 작동 로직은 아래와 같았습니다.

이 작업을 2회씩 세트로 묶어서 작동시켜야했으며, 이를 직렬로 처리해버리면 응답시간이 너무 길어저, 서비스 요건을 충족시키지 못하는 문제가 발생했습니다.

따라서 아래와 같이 병렬처리를 할 수 있도록 바꿔주려고 했습니다…만!

멀티스레딩을 했는데 속도가 그대로다.

하지만..! 병렬처리 후에도 직렬처리 때와 비슷한 수행 시간이 관측되습니다.

로직상으로는 문제가 없었기에, 원인을 찾던 중.. GIL 이라는 게 많이 언급됨을 발견했습니다.

GIL

GIL (Global Interpreter Lock)

Global Interpreter Lock, GIL 은 mutex (상호 배제)의 일종으로 다수의 스레드가 동시에 python 바이트코드를 실행할 수 없게 하는 일종의 잠금장치입니다.

mutex : 멀티 스레딩 혹은 멀티 프로세싱 환경에서, 여러 스레드나 프로세스가 공유 자원에 접근하지 못하게 제한(잠금)을 하는 도구.

좀 더 엄밀하게는 GIL과 mutex 에는 아래와 같은 차이가 있습니다.

항목 GIL Mutex
위치 Python 인터프리터(CPython) 내부 일반적인 스레드/프로세스 동기화 수단
적용 범위 전역 (인터프리터 수준) 코드나 자원 단위
동시 실행 제한 Python 코드 전체 공유 자원만
주 대상 Python 바이트코드 실행 파일, 메모리, 변수 등 공유 자원
대체 가능성 다른 Python 구현체에서는 존재하지 않음 (Jython, PyPy 등) 모든 언어에서 사용 가능

파이썬에서는 왜 GIL을 사용할까?

(1) Python이 설계 및 구현되던 1980년대 ~ 1990년대 초반에는 대부분 프로그램이 단일 스레드였고, 코어 성능이 급증하던 환경으로, 다중 스레드 안전성을 위해 단일 스레드 성능을 희생할 필요가 없었음.
(2) GIL 은 CPython의 메모리 관리를 단순히 만들기 위해 도입된 것임.
(3) 즉, 복잡한 동기화 없이 메모리를 안전하게 다루기 위한 목적.
(4) 여러 스레드가 동시에 참조 카운트를 수정하면 race condition이 발생할 수 있어, 이를 방지하기 위해 한 번에 하나의 스레드만 python 바이트코드를 실행할 수 있게 함.

GIL의 장단점

  • 장점
    -참조 카운팅 기반 모메리 관리의 안전성 확보 : 여러 스레드가 동시에 참조 카운트를 수정하면서 생길 수 있는 race condition 방지.
    -구현의 단순화 : CPython에서 복잡한 락을 사용하지 않고도 스레드 안전성 확보 가능, 특히 단일 스레드에서 성능과 유지보수성 향상
    -개발 속도 및 안정성 : 인터프리터 구현 자체가 단순해지면서 디버깅과 유지보수가 쉬워짐.

  • 단점
    -멀티코어 CPU 성능을 제대로 활용하지 못하면서, CPU-bound 작업에서는 멀티스레드의 이점이 거의 없음.
    -멀티스레딩 병렬 처리 제한 : 대신 멀티프로세싱이나 비동기 프로그래밍 등으로 우회해서 개발 필요

그렇다면 파이썬에서는 멀티스레딩을 사용할 수 없는가?

-Python에서 멀티스레딩의 GIL이 적용되는 영역은 보통 CPU bounding 작업. 즉, I/O bounding 작업에서는 GIL이 영향을 끼치지 않으므로 사용 가능.
-하지만 CPU bounding 업무에서는 CPython이 아닌 다른 인터프리터를 사용하거나, 멀티프로세싱 혹은 비동기 프로그래밍 구조로 설계하는 게 필요하다.

Free Threaded 파이썬에서 GIL 비활성화 하기

Python 3.13

정보를 찾던 와중, 반가운 소식을 발견했다. 바로 Python 3.13 버전부터 GIL을 비활성화 하는 기능을 테스트로 넣고 있다는 것!

-2024년 10월 7일 CPython3.13.0이 출시
-GIL을 비활성화할 수 있는 기능(Free-Threaded CPython)을 실험적으로 지원하기 시작했다.
-Python 버전은 3.13.0b3 버전(테스트 버전)에서 지원된다.

Free Threaded 사용해보기

비교실험을 위해 factorial 함수로 많은 CPU 연산을 하게 하고, 아래 상황들에서의 수행 시간을 체크해보겠습니다.

  • 직렬로 factorial(500000) 을 6회 수행
  • GIL을 활성화하고 병렬(멀티스레딩)로 factorial(500000) 을 6회 수행
  • GIL을 비활성화하고 병렬(멀티스레딩)로 factorial(500000) 을 6회 수행

(1) 직렬 처리

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
# 직렬 처리, Python 3.10.0
from datetime import datetime
import math

# factorail method
def do_factorial(num):
    return math.factorial(num)

# jobs
jobs = [500000, 500000, 500000, 500000, 500000, 500000]

# time check
start = datetime.now()

# serial operation
for job in jobs:
    do_factorial(job)

print(f'time : {datetime.now() - start}')

>> time : 0:00:19.431323

수행 시간 : 19.431323 초

(2) GIL 활성화 - 병렬처리

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
# Multi Threads Python 3.10.0
from datetime import datetime
import math
from concurrent.futures import ThreadPoolExecutor

# factorail method
def do_factorial(num):
    return math.factorial(num)

# jobs
jobs = [500000, 500000, 500000, 500000, 500000, 500000]

# time check
start = datetime.now()

# multi threads operation
with ThreadPoolExecutor() as executor: # 멀티스레딩
    results = list(executor.map(do_factorial, jobs))

print(f'time : {datetime.now() - start}')

>> time : 0:00:16.372969

수행 시간 : 16.372969 초

(3) GIL 비활성화 - 병렬처리

먼저, 3.13.0b3 이상 버전의 파이썬을 설치해줘야 합니다.

python 3.13.0b3 release

테스트 버전을 사용하시 위해 Download free-threaded binaries (experimental) 을 체크하며 설치해주세요.

그러면 파이썬 설치 디렉터리에 python3.13t.exe 라는 실행파일이 설치됩니다.

코드는 위 멀티스레딩 코드와 동일하게 작성하되, py파일로 작성하여 커맨드라인에서 실행할 수 있게 해줘야 합니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# Multi Threads Python 3.10.0
from datetime import datetime
import math
from concurrent.futures import ThreadPoolExecutor

# factorail method
def do_factorial(num):
    return math.factorial(num)

# jobs
jobs = [500000, 500000, 500000, 500000, 500000, 500000]

# time check
start = datetime.now()

# multi threads operation
with ThreadPoolExecutor() as executor: # 멀티스레딩
    results = list(executor.map(do_factorial, jobs))

print(f'time : {datetime.now() - start}')

작성 후 커맨드라인에서 python3.13t.exe 파일로 해당 코드를 실행시켜줍니다.

1
2
3
파이썬설치경로/python3.13t.exe ./gil_test.py

>> time : 0:00:07.700180

수행 시간 : 7.700180 초

결과 정리

구분 직렬처리 GIL 활성화 - 병렬처리(멀티스레딩) GIL 비활성화 - 병렬처리(멀티스레딩)
테스트방식 factorial(500000) x6 동일 동일
소요시간 19.431323 초 16.372969 초 7.700180 초

처리 속도가 매우 빨라진 것을 볼 수 있습니다.
(참고 : 팩토리얼 50만 1회 작업은 4~6초 가량 소요됨)

마치며

아직 테스트 버전이라, 실제 프로덕션 환경에서 적용하기에는 무리가 있습니다.
하지만 곧 나올 3.14 버전부터는 정식으로 Free Threaded 가 적용되지 않을까 기대해봅니다.
그러면 개발할때.. 정말.. 많이 편해질 거 같아요!

Reference

HD 중급자 파이썬 - GIL
GeekNews - Python 3.13에 대해 알아야할 모든 것 - JIT와 GIL의 향상
Today’s Minding - Python에서 GIL을 비활성화 할 수 있다?
Whats New In Python 3.13
Medium Sachin Pal - Disabling GIL Easily in Python3.13