[Python]ThreadBot 프로그래밍

Jimin_Note·2022년 7월 30일
0

[Python]

목록 보기
32/34
post-thumbnail
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)

#잘못된 결과 = 실패
  • 작은 테스트(식탁 100개) 에서는 성공
  • 긴 테스트( 식탁 10000개) 에서는 실패, 결과도 계속 다름

문제의 코드는

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인용 식탁 준비 : 식탁에 네 쌍의 칼과 포크를 준비
# 식탁 정리 : 식탁에서 네 쌍의 칼과 포크를 주방으로 반납
profile
Hello. I'm jimin:)

0개의 댓글