여러 스레드가 하나의 변수에 접근하여 수정할 때, 발생할 수 있는 상황을 이해합니다.
목차
개발 의도
import threading
# 공유 자원
shared_variable = 0
def increment():
global shared_variable
for _ in range(100000):
shared_variable += 1
# 여러 스레드 생성
threads = []
for _ in range(10):
t = threading.Thread(target=increment)
threads.append(t)
t.start()
# 모든 스레드가 종료될 때까지 기다림
for t in threads:
t.join()
print("최종 값:", shared_variable)
# 출력값: 906831(실행할 때마다 다름)
- 위와 같이 10개의 스레드가 increment 함수를 실행하며
shared_variable을 각각 100,000번 증가시키는 코드를 작성합니다.
- 따라서 shared_variable의 최종 출력값은 10 * 100,000 = 1,000,000이 되어야 합니다.
문제 상황
- 코드를 실행했을 때, 1,000,000보다 작은 값이 출력됩니다. (가끔 1,000,000이 출력되기도 함)
- 또한 코드를 실행할 때마다 다른 값이 출력됩니다.
문제 원인 파악
- 여러 스레드가 동시에 shared_variable += 1 연산을 수행하게 되어,
결과 값이 의도한 값과 다르게 나올 수 있습니다.
- 위 코드에서 shared_variable += 1 부분은 사실 여러 단계로 이루어져 있습니다.
1. shared_variable의 현재 "값"을 읽음
2. "값"을 1 증가시킴
3. 증가된 "값"을 shared_variable에 다시 저장
- 이 과정에서 여러 스레드가 동시에 shared_variable의 값을 읽으면,
증가한 값이 덮어써지는 상황이 발생할 수 있습니다.
결과적으로 최종 값이 기대한 값보다 작아지는 오류가 발생합니다.
이러한 문제를 데이터 레이스(Data Race)라고 표현합니다.
스레드 락(Lock)을 통한 문제 해결
import threading
# 공유 자원
shared_variable = 0
lock = threading.Lock()
def increment():
global shared_variable
for _ in range(100000):
with lock: # Lock을 사용해 동기화
shared_variable += 1
# 여러 스레드 생성
threads = []
for _ in range(10):
t = threading.Thread(target=increment)
threads.append(t)
t.start()
# 모든 스레드가 종료될 때까지 기다림
for t in threads:
t.join()
print("최종 값:", shared_variable)
- threading 모듈에서 제공하는 Lock 객체를 사용하여
여러 스레드가 같은 변수에 동시에 접근하지 않도록 막을 수 있습니다.
- lock = threading.Lock() 코드를 통해 락 객체를 생성합니다.
이 객체는 한 번에 하나의 스레드만 shared_variable에 접근할 수 있도록 제어하는 역할을 합니다.
- with lock 코드를 사용해 임계 구역을 정의합니다.
임계 구역(critical section)은 여러 스레드가 동시에 접근하면 안 되는 코드 영역을 의미합니다.
with 블록 안에서 락이 획득되고, 블록을 벗어나면 락이 해제됩니다.
스레드 락(Lock)을 사용한 이유
- 여러 스레드가 동시에 shared_variable을 수정할 경우,
데이터 레이스(Data Race)가 발생할 수 있습니다.
예를 들어, 두 스레드가 동시에 shared_variable += 1을 수행하면
연산 결과가 의도한 값과 다를 수 있습니다.
- lock을 사용하면 한 번에 하나의 스레드만 shared_variable을 수정할 수 있도록 보장됩니다.
lock이 획득되었을 때 다른 스레드는 락이 해제될 때까지 대기합니다.
with lock 구문
lock.acquire()
try:
# 임계 구역
shared_variable += 1
finally:
lock.release()
- 위 코드는 with lock 구문과 동일한 역할을 하지만,
with를 사용하는 방식이 더 직관적이고 안전합니다.
- with lock 구문은 lock.acquire()와 lock.release()를 간단하게 사용할 수 있게 해줍니다.
아래 코드로 디버깅 완료
import threading
# 공유 자원
shared_variable = 0
lock = threading.Lock()
def increment():
global shared_variable
for _ in range(100000):
with lock: # Lock을 사용해 동기화
shared_variable += 1
# 여러 스레드 생성
threads = []
for _ in range(10):
t = threading.Thread(target=increment)
threads.append(t)
t.start()
# 모든 스레드가 종료될 때까지 기다림
for t in threads:
t.join()
print("최종 값:", shared_variable)
- 이 방법을 통해 데이터 레이스를 방지하고 원하는 thread 연산을 수행할 수 있습니다.
- 단, 스레드들이 락이 해제될 때까지 대기하는 시간으로 인해
lock을 사용했을 때, 연산 속도가 조금 느려집니다.
'개발 > 오늘의 개발일지' 카테고리의 다른 글
[Python] 한줄로 print 문을 출력하기 위한 캐리지 리턴 이해하기 (0) | 2024.11.17 |
---|---|
[Python] 스레드 종료를 기다리기 위한 join 이해하기 (0) | 2024.11.16 |
[Python] 메인 프로그램과 함께 종료되는 데몬 스레드 이해하기 (0) | 2024.11.15 |
[Python] 바이트 문자열과 바이트 배열의 차이 이해하기 (0) | 2024.11.13 |
[Python] 반복문과 Lambda: 값 캡처와 변수 참조의 차이 이해하기 (3) | 2024.11.12 |