GIL은 동시 접근을 보장해줄 순 있어도 보호는 하지 못하므로 프로그램의 상태가 오염될 수 있으니 주의해야한다.
#예시: 센서 네트워크에서 광센서를 통해서 빛이 들어온다
class Counter:
def __init__(self):
self.count = 0
def increment(self, offset):
self.count += offset
#센서를 읽을 때 블로킹 I/O를 수행하므로 센서마다 작업자 스레딩 할당
def worker(sensor_index, how_many, counter):
for _ in range(how_many):
#센서를 읽는다.
counter.increment(1)
#병렬로 센서마다 하나씩 worker 스레드 실행 후 모든 스레드가 읽을 때까지 기다림
from threading import Thread
how_many = 10**5
counter = Counter()
threads = []
for i in range(5):
thread = Thread(target=worker, args=(i, how_many, counter))
threads.append(thread)
thread.start()
for thread in threads:
thread.join()
expected = how_many * 5
found = counter.count
print(f'카운터 값은 {expected}여야 하는데, 실제로는 {found} 입니다.')
카운터 값은 500000여야 하는데, 실제로는 373652 입니다.
#파이썬 인터프리터는 모든 스레드를 강제로 공평하게 취급하여 실행시간을 거의 비슷하게 만든다.
#즉, 실행 중인 스레드를 일시 중단시키고 다른 스레드를 실행시키는 일을 반복한다.
#허나, 일시 중단 시점을 알 수 없기에 위와 같은 문제가 생깁니다.
#그래서, 락을 사용하라
from threading import Lock
class LockingCounter:
def __init__(self):
self.lock = Lock()
self.count = 0
def increment(self, offset):
with self.lock:
self.count += offset
count = LockingCounter()
threads = []
for i in range(5):
thread = Thread(target = worker, args=(i,how_many,counter))
threads.append(thread)
thread.start()
for thread in threads:
thread.join()
expected = how_many * 5
found = counter.count
print(f'카운터 값은 {expected}여야 하는데, 실제로는 {found}입니다.')
카운터 값은 500000여야 하는데, 실제로는 722025입니다.
파이썬에는 GIL이 있지만, 파이썬 프로그램 코드는 여전히 여러 스레드 사이에서 일어나는 데이터 경합으로부터 자신을 보호해야 한다.
코드에서 여러 스레드가 상호 배제 락없이 같은 객체를 변경하도록 허용하면 코드가 데이터 구조를 오염시킬 것이다.
여러 스레드 사이에서 프로그램의 불변 조건을 유지하려면 threading 내장 모듈의 Lock 클래스 활용하라