import threading
from queue import Queue
from attr import attrs, attrib
class ThreadBot(threading.Thread): #ThreadBot은 스레드의 하위클래스
def __init__(self):
super().__init__(target=self.manage_table) #스레드 타깃 함수는 manage_table()메소드로 파일 뒷부분에서 정의할 것임
self.cutlery = Cutlery(knives=0, forks=0) #이 봇은 식탁에서 대기하면서 몇 가지 식기를 담당한다. 각 봇은 주방에서 식기를 얻은 후부터 식기를 추적한다.
self.tasks = Queue() #봇은 몇 가지 작업을 할당받는다. 할당된 작업은 봇의 작업 대기(queue)열에 추가되고 봇은 main processing loop를 통해 작업 수행
def manage_table(self):
while True: #봇의 주요 업무는 무한 루프다. 봇을 멈추고 싶다면 shutdown 작업을 전달
task = self.tasks.get() #봇에는 세 가지 작업만 정의되어있다.
if task == 'prepare table': # prepare table작업은 서비스 가능한 새 식탁을 할당받기 위해 해야하는 작업. 주방에서 식기를 받아 식탁에 놓음
kitchen.give(to=self.cutlery, knives=4, forks=4)
elif task == 'clear table': # clear table작업은 식탁을 치워야 할 때 수행하는 작업. 사용한 식기를 주방에 반남해야 함
self.cutlery.give(to=kitchen, knives=4, forks=4)
elif task=='shutdown': # 봇 멈추기
return
@attrs #attrs은 스레드나 asyncio와 상관없는 오픈소스 파이썬 라이브러리로 클래스 생성 가능
class Cutlery:
knives = attrib(default=0) #attrib()함수로 속성 생성 및 기본값 지정을 쉽게 처리 가능 / 보통 기본값 지정은 __init()메소드에서 키워드 인수로 처리하긴 한다.
forks = attrib(default=0)
def give(self, to: 'Cutlery',knives=0, forks=0):
#칼과 포크를 어떤 Cutlery객체에서 다른 Cutlery객체로 옮길 때 사용한다. 새 식탁을 준비하는 봇이 주방에서 식기를 얻을 때나 식탁을 정리한 후에 이 변수에 식기를 반납
self.change(-knives, -forks)
to.change(knives, forks)
def change(self,knives,forks): # 객체 인스턴스의 인벤토리 데이터를 변경하기 위한 매우 간단한 유틸리티 함수
self.knives += knives
self.forks += forks
kitchen = Cutlery(knives=100,forks=100)
#Kitchen을 주방 내의 식기 목록에 대한 변수로 정의. 각 봇은 이 변수에서 식기를 획득. 식탁을 정리한 후에는 이 변수에 식기를 반납
bots = [ThreadBot() for i in range(10)]
#ThreadBot 10개를 사용한다는 뜻
import sys
for bot in bots:
for i in range(int(sys.argv[1])): #식탁 개수는 command-line 매개변수로 입력받는다
bot.tasks.put('prepare table')
bot.tasks.put('clear table')
bot.tasks.put('shutdown') #shutdown 작업을 할당하여 봇들 정지(bot.join()이 대기를 종료하고 반환),이후 나머지 스크립트에서 진단 메세지를 출력하고 봇들을 시작시킨다.
print('Kitchen inventory before service:',kitchen)
for bot in bots:
bot.start()
for bot in bots:
bot.join()
print('Kitchen inventory after service:',kitchen)
# 4인용 식탁 준비 : 식탁에 네 쌍의 칼과 포크를 준비
# 식탁 정리 : 식탁에서 네 쌍의 칼과 포크를 주방으로 반납
100개의 식탁을 여러 ThreadBot으로 동시에 준비하고 정리하기
>>python cutlery_test.py 100
Kitchen inventory before service: Cutlery(knives=100, forks=100)
Kitchen inventory after service: Cutlery(knives=100, forks=100)
#서비스 전과 후 칼과 포크 갯수가 동일 = 성공!
10000개의 식탁을 여러 ThreadBot으로 동시에 준비하고 정리하기
>>python cutlery_test.py 10000
Kitchen inventory before service: Cutlery(knives=100, forks=100)
Kitchen inventory after service: Cutlery(knives=96, forks=96)
>>python cutlery_test.py 10000
Kitchen inventory before service: Cutlery(knives=100, forks=100)
Kitchen inventory after service: Cutlery(knives=100, forks=92)
>>python cutlery_test.py 10000
Kitchen inventory before service: Cutlery(knives=100, forks=100)
Kitchen inventory after service: Cutlery(knives=104, forks=100)
#잘못된 결과 = 실패
문제의 코드는
def change(self,knives,forks):
self.knives += knives
self.forks += fork
-> 경합 조건 오류의 전형적인 징후
인라인 합산인 +=
은 내부적으로 몇 가지 단계로 구성되어 있다.
1. self.knives
에서 현재 값을 읽어 임시 저장소에 저장
2. knives
의 값을 임시 저장소 내의 값에 합산
3. 임시 저장소 내의 값을 복제하여 원래의 저장소인 self.knives
에 저장
선점형 멀티태스킹의 문제는 이런 단계를 실행 중인 스레드가 언제든 중단(interrupt)된 후 다른 스레드에서 해당 단계가 실행될 수 있다는 점
ex)
ThreadBot A가 1단계를 수행중이다.
OS가 A를 정지시키고 B로 콘텍스트 전환을 수행했다.
B도 1단계에 따라 self.knives의 현재값을 읽어 들인다.
또 OS가 B를 정시시키고 A의 실행을 재개한다.
A는 2,3단계를 실행하고, B의 실행을 재개한다.
하지만 B는 A에 의해 중단된 위치(1단계 이후)부터 2,3단계를 실행한다.
따라서 A에 의해 실행결과가 분실된다.
이 문제는 공유되는 상태 값에 대해 변경을 수행하는 코드 주변에 lock을 둘러 수정할 수 있다
def change(self,knives,forks):
with self.lock:
self.knives += knives
self.forks += fork
🌟 수정 후 코드
import threading
from queue import Queue
from attr import attrs, attrib
class ThreadBot(threading.Thread): #ThreadBot은 스레드의 하위클래스
def __init__(self):
super().__init__(target=self.manage_table) #스레드 타깃 함수는 manage_table()메소드로 파일 뒷부분에서 정의할 것임
self.cutlery = Cutlery(knives=0, forks=0) #이 봇은 식탁에서 대기하면서 몇 가지 식기를 담당한다. 각 봇은 주방에서 식기를 얻은 후부터 식기를 추적한다.
self.tasks = Queue() #봇은 몇 가지 작업을 할당받는다. 할당된 작업은 봇의 작업 대기(queue)열에 추가되고 봇은 main processing loop를 통해 작업 수행
def manage_table(self):
while True: #봇의 주요 업무는 무한 루프다. 봇을 멈추고 싶다면 shutdown 작업을 전달
task = self.tasks.get() #봇에는 세 가지 작업만 정의되어있다.
if task == 'prepare table': # prepare table작업은 서비스 가능한 새 식탁을 할당받기 위해 해야하는 작업. 주방에서 식기를 받아 식탁에 놓음
kitchen.give(to=self.cutlery, knives=4, forks=4)
elif task == 'clear table': # clear table작업은 식탁을 치워야 할 때 수행하는 작업. 사용한 식기를 주방에 반남해야 함
self.cutlery.give(to=kitchen, knives=4, forks=4)
elif task=='shutdown': # 봇 멈추기
return
@attrs #attrs은 스레드나 asyncio와 상관없는 오픈소스 파이썬 라이브러리로 클래스 생성 가능
class Cutlery:
lock = threading.Lock() #lock 사용하기 위한 스트립트
knives = attrib(default=0) #attrib()함수로 속성 생성 및 기본값 지정을 쉽게 처리 가능 / 보통 기본값 지정은 __init()메소드에서 키워드 인수로 처리하긴 한다.
forks = attrib(default=0)
def give(self, to: 'Cutlery',knives=0, forks=0):
#칼과 포크를 어떤 Cutlery객체에서 다른 Cutlery객체로 옮길 때 사용한다. 새 식탁을 준비하는 봇이 주방에서 식기를 얻을 때나 식탁을 정리한 후에 이 변수에 식기를 반납
self.change(-knives, -forks)
to.change(knives, forks)
# def change(self,knives,forks): # 객체 인스턴스의 인벤토리 데이터를 변경하기 위한 매우 간단한 유틸리티 함수
# self.knives += knives
# self.forks += forks #경합 조건 오류
def change(self,knives,forks): # 객체 인스턴스의 인벤토리 데이터를 변경하기 위한 매우 간단한 유틸리티 함수
with self.lock: #공유되는 값에 대해 변경을 수행하는 코드 주변에 lock을 둘러주기
self.knives += knives
self.forks += forks
kitchen = Cutlery(knives=100,forks=100)
#Kitchen을 주방 내의 식기 목록에 대한 변수로 정의. 각 봇은 이 변수에서 식기를 획득. 식탁을 정리한 후에는 이 변수에 식기를 반납
bots = [ThreadBot() for i in range(10)]
#ThreadBot 10개를 사용한다는 뜻
import sys
for bot in bots:
for i in range(int(sys.argv[1])): #식탁 개수는 command-line 매개변수로 입력받는다
bot.tasks.put('prepare table')
bot.tasks.put('clear table')
bot.tasks.put('shutdown') #shutdown 작업을 할당하여 봇들 정지(bot.join()이 대기를 종료하고 반환),이후 나머지 스크립트에서 진단 메세지를 출력하고 봇들을 시작시킨다.
print('Kitchen inventory before service:',kitchen)
for bot in bots:
bot.start()
for bot in bots:
bot.join()
print('Kitchen inventory after service:',kitchen)
# 4인용 식탁 준비 : 식탁에 네 쌍의 칼과 포크를 준비
# 식탁 정리 : 식탁에서 네 쌍의 칼과 포크를 주방으로 반납