서론
Python을 사용하는 많은 개발자들이 GIL(Global Interpreter Lock)에 대해 오해하고 있다. 특히 "GIL이 있어서 Python은 멀티스레드 환경에서 레이스 컨디션으로부터 안전하다"는 생각은 매우 위험한 오해이다. 이 글에서는 실제 코드를 통해 GIL이 레이스 컨디션을 완전히 방지하지 못한다는 것을 보여준다.
GIL이란 무엇인가?
GIL은 Python 인터프리터가 한 번에 하나의 스레드만 Python 바이트코드를 실행할 수 있도록 제한하는 메커니즘이다. 이는 Python의 메모리 관리와 C 확장 모듈의 안전성을 보장하기 위해 존재한다.
Race Condition은 무엇인가?
Race Condition(경쟁 조건)은 두 개 이상의 작업이 동시에 실행되면서 공유 자원에 접근할 때, 실행 순서에 따라 결과가 달라지는 상황을 말한다.
간단히 예를 들면:
- 두 개의 쓰레드가 같은 변수 x를 동시에 수정하려고 할 때,
- 쓰레드 A는 x += 1, 쓰레드 B도 x += 1을 실행하지만,
- 순서에 따라 x 값이 1만 증가하거나, 2만큼 제대로 증가할 수도 있다.
이런 예측 불가능한 결과가 바로 Race Condition의 문제이다.
실험: GIL이 레이스 컨디션을 완전히 방지하는가?
다음과 같은 간단한 코드로 실험을 진행해본다:
import threading
# Shared variables
counter = 0
try_count = 1000
def increment_counter():
global counter
for _ in range(try_count):
# Complex operation to increase chance of GIL switching to another thread
temp = counter
# I/O operation to increase chance of GIL release
with open('temp.txt', 'a') as f:
f.write('1')
counter = temp + 1
def main():
thread_num = 8
# Create threads
threads = []
for _ in range(thread_num):
thread = threading.Thread(target=increment_counter)
threads.append(thread)
thread.start()
# Wait for all threads to complete
for thread in threads:
thread.join()
# Print the final counter value
print(f"Final counter value: {counter}")
print(f"Expected value: {try_count * thread_num}")
if __name__ == "__main__":
main()
이 코드는 8개의 스레드가 각각 1000번씩 카운터를 증가시키는 작업을 수행한다. 이론적으로는 최종 카운터 값이 8000(1000 × 8)이 되어야 한다.
결과 분석
이 코드를 실행하면 다음과 같은 결과를 얻을 수 있다:
Final counter value: 1001
Expected value: 8000
왜 이런 현상이 발생하는가?
I/O 작업 중의 GIL 해제파일 I/O 작업은 GIL을 해제한다.
이는 다른 스레드가 실행될 수 있는 기회를 제공한다.
읽기-수정-쓰기 패턴의 문제
temp = counter
with open('temp.txt', 'a') as f:
f.write('1')
counter = temp + 1
여러 스레드가 동시에 같은 temp 값을 읽을 수 있다.
I/O 작업 중에 다른 스레드가 실행되어 같은 값을 읽고 수정할 수 있다.
스레드 수의 영향
스레드 수가 많을수록 레이스 컨디션 발생 가능성이 높아진다.
8개의 스레드가 동시에 같은 자원에 접근하려고 시도하기 때문이다.
GIL의 한계
GIL은 다음과 같은 경우에서 레이스 컨디션을 방지하지 못한다:
1. I/O 작업이 포함된 경우
2. C 확장 모듈이 GIL을 해제하는 경우
3. 복잡한 연산이 여러 단계로 나뉘어 있는 경우
레이스 컨디션 방지 방법
Lock 사용
lock = threading.Lock()
with lock:
counter += 1
Queue 사용
from queue import Queue
q = Queue()
q.put(1)
Async / Await 사용
async / await을 이용한다면 훨씬 더 우아하게 이 문제를 해결할 수 있다.
https://github.com/insooneelife/PythonExamples/blob/master/race_condition/async_example.py
결론
Python의 GIL은 모든 레이스 컨디션을 방지하지 않는다. 특히 I/O 작업이 포함된 경우나 복잡한 연산이 여러 단계로 나뉘어 있는 경우에는 레이스 컨디션이 발생할 수 있다. 따라서 멀티스레드 프로그래밍을 할 때는 항상 적절한 동기화 메커니즘을 사용해야 한다.
'프로그래밍 > Python' 카테고리의 다른 글
[Python] PyInstaller, PyArmor를 이용한 파이썬 패키징 (0) | 2024.08.15 |
---|---|
[Python] literal_eval을 이용한 외부 python 파일 읽기 (0) | 2024.08.15 |
[Python] Python의 모듈 참조 (0) | 2024.05.29 |