[asyncio] 파이썬 쓰레드

Hyeseong·2022년 4월 11일
0

asyncio

목록 보기
1/1
import threading
import sys

from queue import Queue
from attr import attrs, attrib


# ThreadBot programming for table service
class ThreadBot(threading.Thread):
    # A ThreadBot is a subclass of a thread
    def __init__(self):
        # The target function of the thread is the manage_table() method,
        # defined later in the file.
        super().__init__(target=self.manage_table)
        # This bot is going to be waiting tables and will need to be
        # responsible for some cutlery. Each bot keeps track of the cutlery
        # that it took from the kitchen here. (The Cutlery class will be
        # defined later.)
        self.cutlery = Cutlery(knives=0, forks=0)
        # The bot will also be assigned tasks. They will be added to this task
        # queue, and the bot will perform them during its main processing loop,
        # next.
        self.tasks = Queue()

    def manage_table(self):
        # The primary routine of this bot is this infinite loop. If you need to
        # shut down a bot, you have to give them the shutdown task.
        while True:
            task = self.tasks.get()
            if task == "prepare table":
                # There are only three tasks defined for this bot. This one,
                # prepare table, is what the bot must do to get a new table
                # ready for service. For our test, the only requirement is to
                # get sets of cutlery from the kitchen and place them on the
                # table. clear table is used when a table is to be cleared:
                # the bot must return the used cutlery back to the kitchen.
                # shutdown just shuts down the bot.
                kitchen.give(to=self.cutlery, knives=4, forks=4)
            elif task == "clear table":
                self.cutlery.give(to=kitchen, knives=4, forks=4)
            elif task == "shutdown":
                return

1) ThreadBot은 스레드의 하위 클래스이다.
2) 스레드의 타깃 함수는 manage_table() 메서드입니다.
3) 이 봇은 식탁에서 대기하면서 몇가지 식기를 담당합니다. 각 봇은 주방에서 식기를 얻은 후 부터 식기를 추적합니다.
4) 봇은 몇 가지 작업을 할당 받습니다. 할당된 작업은 봇의 작업 대기열(queue)에 추가되고 봇은 주 처리 루프(main processing loop)를 통해 작업을 수행합니다.
5) 봇의 주요 업무는 무한 루프다. 봇을 멈추고 싶다면 shutdown 작업을 전달해야 한다.
6) 봇에는 세 가지 작업만 정의 되어 있다.

  • prepare table작업은 서비스 가능한 새 식탁을 할당받기 위해 해야하는 작업이다. 필요 사항은 주방에서 식기를 받아 식탁에 놓는 것이다.
  • clear table 작업은 식탁을 치워야 할 때 수행해야 하는 작업이다. 그 과정에서 봇은 사용한 식기를 주방에 반납해야 한다.
  • shutdown 작업은 봇을 즉시 종료시키는 작업이다.
@attrs
class Cutlery:
    # The attrib() function provides an easy way to create attributes,
    # including defaults, which you might normally have handled as keyword
    # arguments in the __init__() method.
    knives = attrib(default=0)
    forks = attrib(default=0)
    lock = attrib(threading.Lock())

    # This method is used to transfer knives and forks from one Cutlery object
    # to another. Typically, it will be used by bots to obtain cutlery from the
    # kitchen for new tables, and to return the cutlery back to the kitchen
    # after a table is cleared.
    def give(self, to: "Cutlery", knives=0, forks=0):
        self.change(-knives, -forks)
        to.change(knives, forks)

    # This is a very simple utility function for altering the inventory data
    # in the object instance.
    def change(self, knives, forks):
        # with self.lock:
        with self.lock:
            self.knives += knives
            self.forks += forks


# We’ve defined kitchen as the identifier for the kitchen inventory of
# cutlery. Typically, each of the bots will obtain cutlery from this
# location. It is also required that they return cutlery to this store when a
# table is cleared.
kitchen = Cutlery(knives=100, forks=100)
# This script is executed when testing. For our test, we’ll be using 10
# ThreadBots.
bots = [ThreadBot() for i in range(10)]

for bot in bots:
    # We get the number of tables as a command-line parameter, and then give
    # each bot that number of tasks for preparing and clearing tables in the
    # restaurant.
    for i in range(int(sys.argv[1])):
        bot.tasks.put("prepare table")
        bot.tasks.put("clear table")
    # The shutdown task will make the bots stop (so that bot.join() a bit
    # further down will return). The rest of the script prints diagnostic
    # messages and starts up the bots.
    bot.tasks.put("shutdown")
print("Kitchen inventory before service:", kitchen)
for bot in bots:
    bot.start()
for bot in bots:
    bot.join()
print("Kitchen inventory after service:", kitchen)

1) attrs는 스레드나 asyncio와 상광없는 오픈소스 파이썬 라이브러리로 클래스를 쉽게 생성할 수 있다.
@attrs 데코레이터를 통해 Cutlery클래스에 일반적인 상용구 코드(boilterplate code, 예를들어 init() )를 자동으로
포함 시킬 수 있다.

2) attrib() 함수로 속성 생성 및 기본값 지정을 쉽게 처리 할 수 있다. 보통 기본값 지정은 init() 메서드에서 키워드 인수로
처리했을 것이다.

3) 이 메서드는 칼과 포크를 어떤 Cutlery객체에서 다른 Cultery 객체로 옮길 때 사용한다.
보통 새 식탁을 준비하기 위해 봇이 주방에서 식기를 얻을 때 사용될 것이다. 또한 식탁 을 정리한 후 주방에서 반납할 때도 사용될 것이다.

4) 객체 인스턴스의 인벤토리 데이터를 변경하기 위한 매우 간단한 유틸리티 함수이다.

5) kitchen을 주방 내의 식기 목록에 대한 변수로 정의했다. 각 봇은 이 변수에서 식기를 획득한다. 식탁을 정리한 후에는 이 변수에 식기를 반납한다.

6) 이 스크립트는 테스트할 때 실행된다. 테스트 시 10개의 ThreadBot을 사용 할 것이다.

7) 식탁 개수는 명령줄(command-line)매개변수로 입력 받는다. 전체 식탁을 준비하고 정리하는 작업을 각 봇에게 나눠 할당한다.

8) shutdown 작업을 할당하여 봇들을 정지시킨다.(즉, 아래 줄에 있는 bot.join()이 대기를 종료하고 반환한다.)
이후 나머지는 스크립트에서 진단 메시지를 출력하고 봇들을 시작시킨다.

상황 요약
1) ThreadBot code 읽기 쉽고 logic도 괜찮다.
2) 작은 테스트(100, 1000)는 성공
3) 더 긴 테스트(10,000)는 실패

  • 이는 경합 조건(race condition)의 전형적인 징후다.
def change(self, knives, forks):
    self.knives += knives
    self.forks += forks

인라인 합산인 +=는 내부적으로 몇 가지 단계로 구현되어 있다.(이 코드는 파이썬 인터프리터 내부의 C코드다)
1) self.knives에서 현재 값을 읽어 들여 임시 저장소에 저장한다.
2) knives의 값을 임시 저장소 내의 값에 합산한다.
3) 임시 저장소 내의 값을 복제하여 원래의 저장소인 self.knives에 저장한다.

선점형 멀티태스킹의 문제는 이런 단계를 실행 중인 스레드가 언제든 중단된(interupt) 후 다른 스레드에서 해당 단계가 실행될 수 있다는 점이다.

예를들어 ThreadBot A가 1단계를 수행하고 있다고 하자. 그런데 OS 스케줄러가 A를 정지시키고 ThreadBot B로 컨텍스트 전환을 수행한다.
B도 1단계에 따라 self.knives의 현재 값을 읽어 들인다. 그리고 OS가 B를 일시 중지시키고 A의 실행을 재개한다.
A는 2, 3단계를 실행하고, B의 실행을 재개한다. 하지만 B는 A에 의해 중단된 위치(1단계 이후)부터 2,3단계를 실행한다.
따라서 A에 의해 실행 결과가 분실된다.
"""

이 문제는 공유되는 상태 값에 대해 변경을 수행하는 코드 주변에 락을 둘러 수정 할 수 있다.
(Cutlery 클래스에 threading.Lock을 추가

def change(self, knives, forks):
    with self.lock:
        self.knives += knives
        self.forks += forks

단점 :
- 하지만 이 방법을 적용하기 위해서는 여러 스레드 간에 공유되는 상태 값이 쓰이는 모든 위치를 파악해야한다.
- 모든 소스 코드를 통제해야함.
- 3rd party library 사용시 적용하기 매우 어려움
- 오픈소스 생태계가 매우 거대한 파이썬에서는 정말 어렵문
- 소스 코드만 확인해서는 경합 조건을 찾아내기 힘들다는 점
- 보통 소스 코드내에서 스레드 간 콘텍스트 전환이 발생하는 부분에 대한 힌트가 없기 때문이다.
- OS는 거의 모든 곳에서 스레드 간의 콘텍스트 전환을 수행 할 수 있기 때문.
장점 :
- 코드 한 줄 추가하고 인덴트만 넣어주만 해결!

다른 해결 방법:
- 단 하나의 ThreadBot을 사용하여 모든 식탁을 처리하도록 하는 것.
kitchen 변수의 칼과 포크를 하나의 스레드에 의해 수정되도록 하는 것이다.

- 비동기 프로그램에서는 여러 병행 코루틴 간에 콘텍스트 전환이 정확히 언제 발생하는지 확인 할 수 있음.
- await 키워드를 통해서 명시적으로 표시하기 때문
profile
어제보다 오늘 그리고 오늘 보다 내일...

0개의 댓글