스레드 동기화(Thread Synchronization)
여러 스레드가 동시에 실행되면 데이터 불일치 문제가 발생할 수 있습니다. 동기화를 사용하면 이 문제를 방지할 수 있습니다. 동기화는 데이터 불일치 문제를 극복하기 위해 한 번에 하나의 스레드만 실행할 수 있도록 합니다. 동기화는 실행해야 하는 스레드가 둘 이상인 경우 한 번에 단일 스레드가 임계 영역에 액세스할 수 있도록 보장하는 메커니즘입니다.
동기화 프리미티브를 보장하는 동안 교착 상태 및 경쟁 조건과 같은 두 가지 주요 문제가 발생할 수 있습니다.
다음 스크립트는 두 스레드가 함께 실행되어 불규칙하게 출력되는 모습을 보여주고 있습니다.
from threading import *
import time
def func(n):
for i in range(2):
print("Hi:", end='')
time.sleep(2)
print(n)
t1 = Thread(target = func, args = ("Park", ))
t2 = Thread(target = func, args = ("Kim", ))
t1.start()
t2.start()
Output:
Hi:Hi:ParkKim
Hi:
Hi:Kim
Park
Lock 사용
Lock에는 두 가지 정의된 상태가 있습니다. 하나는 잠금 상태이고 다른 하나는 잠금 해제 상태입니다.
Lock은 초기 생성 시에 잠금 해제 상태로 생성됩니다. 지원되는 두 가지 메서드는 acquire() 및 release()가 있습니다.
Lock을 acquire()하면 해당 스레드만 공유 데이터에 접근할 수 있고 Lock를 release()해야만 다른 스레드에서 공유 데이터에 접근 할 수 있습니다.
acquire()
- 잠금 상태인 경우, 다른 스레드에서 release()호출을 통해서 잠금 해지 상태로 바꿀 때까지 블록 됩니다. 잠금 상태에서 acquire() 메서드를 호출하면 다시 잠금으로 재설정하고 반환합니다.
- 잠금 해지 상태인 경우, 잠금 상태로 변경하고 반환합니다.
release()
- 잠금 상태인 경우, 잠금 해지 상태로 변경합니다.
- 잠금 해지 상태인 경우, RuntimeError가 발생합니다.
다음 스크립트는 잠금 해지 상태에서 release()를 호출했을 때 발생하는 오류입니다.
from threading import *
lock = Lock()
lock.release()
Output:
RuntimeError: release unlocked lock
다음 스크립트는 acquire()와 release()를 사용하는 방법을 보여주고 있습니다. 두 스레드가 순차적으로 출력하고 있는 모습을 보여주고 있습니다.
from threading import *
import time
lock = Lock()
def func(n):
lock.acquire()
for i in range(2):
print("Hi:", end='')
print(n)
lock.release()
t1 = Thread(target = func, args = ("Park", ))
t2 = Thread(target = func, args = ("Kim", ))
t1.start()
t2.start()
Output:
Hi:Park
Hi:Park
Hi:Kim
Hi:Kim
RLock 사용
스레드가 재귀적으로 리소스에 액세스하면 스레드가 동일한 잠금을 다시 획득하여 스레드가 차단될 수 있습니다. 따라서 Lock 잠금 방법은 재귀 함수 실행에 적합하지 않습니다. 이러한 문제를 처리하기 위해 재진입 잠금(RLock)이 선호됩니다.
재진입 잠금은 동일한 스레드가 여러 번 획득할 수 있는 동기화 프리미티브입니다.
기본 잠금에서 사용되는 잠금 또는 잠금 해제 상태 외에 스레드 및 재귀 수준을 소유한다는 개념을 따릅니다.
잠금 상태의 경우 일부 스레드가 잠금을 소유하는 반면 잠금 해제 상태의 경우 잠금을 소유하는 스레드가 없습니다.
스레드는 잠금을 수행하기 위해 acquire() 메서드를 호출하고 반환합니다.
스레드는 잠금을 해제하기 위해 release() 메서드를 호출합니다.
from threading import *
lock = RLock()
def fact(n):
lock.acquire()
if n == 0:
opt = 1
else:
opt = n * fact(n-1)
lock.release()
return opt
def opt(n):
print("Factorial", n, ":", fact(n))
t1 = Thread(target = opt, args = (3, ))
t2 = Thread(target = opt, args = (5, ))
t1.start()
t2.start()
Output:
Factorial 3 : 6
Factorial 5 : 120
다음 표는 Lock과 Rlock의 차이를 보여줍니다.
Lock | Rlock |
- 메인 스레드를 포함하여 한 번에 하나의 스레드만 획득할 수 있습니다. - 재귀 및 중첩 함수에서는 선호되지 않습니다. - 객체의 잠금 및 잠금 해제만 담당합니다. 메인 쓰레드와 재귀 수준을 고려하지 않습니다. |
- 한 번에 하나의 스레드만 획득할 수 있지만 메인 스레드는 여러 번 획득할 수 있습니다. - 재귀 및 중첩 함수에 가장 적합합니다. - 잠금, 잠금 해제, 메인 스레드 및 재귀 수준도 담당합니다. |
세마포어 사용
세마포어는 공유 리소스의 액세스를 제한하는 데 선호됩니다.
세마포어는 모든 acquire() 호출에 의해 감소되고 모든 release() 호출에 의해 증가되는 내부 카운터를 처리합니다. 카운터는 0보다 작을 수 없습니다. 카운터는 동시에 액세스할 수 있는 최대 스레드 수를 나타냅니다. 기본값은 1입니다.
기본 구문은 다음과 같습니다.
(세마포어 객체 생성) = Semaphore(counter)
사용 예)
s = Semaphore()
여기에서 카운터 값은 1이고 한 번에 하나의 스레드에만 액세스가 허용됩니다.
s = Semaphore(3)
여기서 세마포어 객체는 세 개의 스레드에서 동시에 액세스할 수 있습니다. 나머지 스레드는 세마포어가 해제될 때까지 기다려야 합니다.
다음 스크립트는 세마포어의 카운터를 1로 설정하고 수행한 예제입니다. 카운터가 1이므로 한번에 하나의 스레드에만 액세스 할 수 있어서 순차적으로 출력됨을 보여주고 있습니다.
from threading import *
import time
s = Semaphore(1)
def func(n):
s.acquire()
for i in range(2):
print("Hi:", end='')
time.sleep(2)
print(n)
s.release()
t1 = Thread(target = func, args = ("Park", ))
t2 = Thread(target = func, args = ("Kim", ))
t1.start()
t2.start()
Output:
Hi:Park
Hi:Park
Hi:Kim
Hi:Kim
다음 스크립트는 세마포어의 카운터를 2로 설정하고 수행한 예제입니다. 두개의 스레드에 동시에 접근하므로 불규칙하게 출력됩니다.
from threading import *
import time
s = Semaphore(2)
def func(n):
s.acquire()
for i in range(2):
print("Hi:", end='')
time.sleep(2)
print(n)
s.release()
t1 = Thread(target = func, args = ("Park", ))
t2 = Thread(target = func, args = ("Kim", ))
t1.start()
t2.start()
Output:
Hi:Hi:KimPark
Hi:
Hi:Park
Kim
'프로그래밍 언어 > 파이썬 (Python)' 카테고리의 다른 글
[파이썬 학습] GUI 생성 - 1 (0) | 2022.02.28 |
---|---|
[파이썬 학습] 스레드 간 통신 (0) | 2022.02.27 |
[파이썬 학습] 멀티 스레딩 (0) | 2022.02.25 |
[파이썬 학습] 사용자 모듈 (0) | 2022.02.23 |
[파이썬 학습] Datetime, Math, Random 모듈 (0) | 2022.02.22 |