Thread (2)

teal·2023년 8월 9일
0

Python

목록 보기
2/8

그런데 지난편처럼 lock을 사용할때는 조심해야하는 점이 존재한다.
바로 교착상태(deadlock)인데 서로의 락이 풀리길 기다리느라 이도저도 못한 상태가 발생할 수 있다는 점이다.

이전 편의 캐릭터가 이제 던전에 사냥을 간다고 생각하자

class Dungeon:
    def __init__(self, reward: int, *args, **kwargs):
        self._reward: int = reward
        self._field_lock: Lock = Lock()

    def get_reward(self) -> int:
        return self._reward

    def get_lock(self) -> Lock:
        return self._field_lock

던전이 생겼고 한 던전에는 한 캐릭터만 사냥을 해야하기때문에 _field_lock이라는 락도 필요하다.

    def hunt_multiple_times_in_dungeons(self, dungeons: List[Dungeon], times: int):
        dungeon_length: int = len(dungeons)
        dungeons[0].get_lock().acquire()
        for i in range(len(dungeons)):
            for _ in range(times):
                self.work(dungeons[i].get_reward())

            if i != dungeon_length-1:
                dungeons[i+1].get_lock().acquire()
            
            dungeons[i].get_lock().release()

위와같이 캐릭터가 여러 던전들에서 사냥하는 메소드도 추가되었다.
다음 던전에 대한 소유권을 얻기 전에는 지금 사냥하고 있는 던전의 소유권을 포기하지 않는다.

먼저 내 캐릭터가 혼자 사냥하는 경우는 괜찮다.

my_charater = Character()
dungeons = [Dungeon(10), Dungeon(15)]

hunting_in_dungeon = Thread(target=my_charater.hunt_multiple_times_in_dungeons,
                            kwargs={"dungeons": [dungeons[0], dungeons[1]], "times": 5000000})

start_time = time.time()
hunting_in_dungeon.start()
hunting_in_dungeon.join()

print(time.time()-start_time)
print(my_charater.show_gold())
6.609790086746216
125000000

두 던전을 각각 500만번씩 돌면서 골드를 획득했다.

그런데 만약 다른 캐릭터가 던전을 같이 돌기 시작하면 문제가 발생한다.

my_charater = Character()
other_charater = Character()
dungeons = [Dungeon(10), Dungeon(15)]

hunting_my_charater = Thread(target=my_charater.hunt_multiple_times_in_dungeons,
                            kwargs={"dungeons": [dungeons[0], dungeons[1]], "times": 5000000})

hunting_other_charater = Thread(target=other_charater.hunt_multiple_times_in_dungeons,
                            kwargs={"dungeons": [dungeons[1], dungeons[0]], "times": 5000000})
start_time = time.time()
hunting_my_charater.start()
hunting_other_charater.start()

hunting_my_charater.join()
hunting_other_charater.join()

print(time.time()-start_time)
print(my_charater.show_gold())

위처럼 내 캐릭터는 0, 1 던전을 돌기 시작하고 다른 캐릭터는 1, 0 순으로 던전에서 사냥한다고 할때 내 캐릭터가 0던전을 끝내고 1던전을 가기위해 소유권을 얻으려고 하지만 다른 캐릭터는 1던전의 소유권을 가진채로 0던전의 소유권을 얻기 위한 데드락 상태가 발생하게된다.

이는 deadlock의 발생요건 4가지를 만족했기 때문이다.

위키에 따르면

  1. 상호 배제(Mutual exclusion)

    • 한 리소스는 한 번에 한 프로세스만이 사용할 수 있음
  2. 점유와 대기(Hold and wait)

    • 어떤 프로세스가 하나 이상의 리소스를 점유하고 있으면서 다른 프로세스가 가지고 있는 리소스를 기다리고 있음
  3. 비선점(No preemption)

    • 프로세스가 태스크를 마치고 리소스를 자발적으로 반환할 때까지 기다림
  4. 환형 대기(Circular wait)

    • Hold and wait 관계의 프로세스들이 서로를 기다림

이 4가지 조건을 만족해야 교착 상태가 발생한다.
1.상호 배제(Mutual exclusion)의 경우

  • 아래처럼 사냥할때 해당 사냥터에 대한 소유권을 먼저 얻기 때문에 다른 캐릭터가 사냥하고 있을때는 해당 던전에서 사냥을 시작하지 못한다. 그래서 상호 배제를 만족한다.

           dungeons[0].get_lock().acquire()
           for i in range(len(dungeons)):
               for _ in range(times):
                   self.work(dungeons[i].get_reward())
    
               if i != dungeon_length-1:
                   dungeons[i+1].get_lock().acquire()
               
               dungeons[i].get_lock().release()

2.점유와 대기(Hold and wait)의 경우

  • 내 캐릭터는 0던전에 대한 소유권을 가지고 있고 1던전을 기다리고 있는 상태이다. 이 상태를 통해 점유와 대기를 만족한다.

3.비선점(No preemption)의 경우

  • 다른 캐릭터가 사냥하고 있다고 하더라도 내 캐릭더가 다른 캐릭터를 치워버리고 그 던전에서 사냥하는 경우는 없기 때문에 비선점의 경우도 만족한다.

4.환형 대기(Circular wait)의 경우

  • 내 캐릭터는 0던전에 대한 소유권을 가지고 있으면서 1던전을 기다리는 점유와 대기를 만족하고 상대 캐릭터도 똑같이 1던전에 대한 소유권을 가진채로 0던전을 기다리기 때문에 점유와 대기를 만족하는 상태에서 서로가 가지고 있는 던전에 대한 소유권을 두고 서로를 기다리기 때문에 환형 대기도 만족하게 된다.

위 4가지 조건을 모두 만족하기 때문에 데드락이 발생했고 저 4개중 하나만 만족하지 않아도 deadlock 상태에 빠지지 않게 된다.

  1. 상호 배제
    • 일단 한 던전에서 한 캐릭터만 사냥해야하므로 상호 배제는 풀지 못한다.
  2. 점유와 대기
    • 가장 간단한 해결방법으로 다음 던전을 기다릴때는 지금 사냥을 마친 던전의 소유권을 포기하면 된다. 그러면 점유를 하고 있지 않으므로 이 조건을 만족하지 않는다.
  3. 비선점을 선점으로 바꾼다
    • 더 강한 캐릭터가 약한 캐릭터의 자리를 뺏도록 할 수 있다.
  4. 환형 대기
    • 이 경우는 점유와 대기가 만족하지 않게 되면 자연스럽게 사라진다.

점유와 대기를 푸는 방법으로 아래와 같이 사냥 방식을 변경한다.

    def hunt_multiple_times_in_dungeons(self, dungeons: List[Dungeon], times: int):
        for i in range(len(dungeons)):
            dungeons[i].get_lock().acquire()
            for _ in range(times):
                self.work(dungeons[i].get_reward())
            dungeons[i].get_lock().release()

서로를 배려하는 좋은 방법을 통해

9.861868143081665
my_charater 125000000
other_charater 125000000

내 캐릭과 다른 캐릭 모두 좋은 수익을 얻을 수 있었다.

만약 내 캐릭터가 우선순위를 갖게해서 뺏게 만들고자한다면

    def hunt_multiple_times_in_dungeons(self, dungeons: List[Dungeon], times: int):
        for i in range(len(dungeons)):
            other_charater = dungeons[i].entered_character
            if other_charater is not None:
                if self.show_power() > other_charater.show_power():
                    dungeons[i].is_character_banned = True

            dungeons[i].get_lock().acquire()
            dungeons[i].entered_character = self
            for _ in range(times):
                if dungeons[i].is_character_banned == True:
                    dungeons[i].is_character_banned = False
                    dungeons[i].entered_character = None
                    dungeons[i].get_lock().release()
                    time.sleep(0.1)
                    dungeons[i].get_lock().acquire()
                    dungeons[i].entered_character = self
                self.work(dungeons[i].get_reward())
            dungeons[i].entered_character = None
            dungeons[i].get_lock().release()

위와같이 변경해서 던전에서 자리 뺏는 플래그를 설정하고 반복문시 해당 플래그를 체크해서 뺏길시 잠시 락을 풀고 다른 캐릭터에게 락을 넘긴다음 다시 해당 던전에 락을 기다리는 방식으로 구현이 가능했다.

my_charater 7.369524002075195
other_charater 11.597201347351074
my_charater 125000000
other_charater 125000000

내 캐릭터는 혼자 사냥할때와 큰 차이없이 쾌적한 사냥을 즐기게 되었다.

profile
고양이를 키우는 백엔드 개발자

0개의 댓글