본문 바로가기
Python/Tip & Etc

python Thread(쓰레드) 병렬처리2(답안)

by To올라운더 2023. 11. 18.
반응형

해당 포스팅은 지난 시간에 진행된 쓰레드에 관련된 문제를 해결하는 포스팅입니다.

2023.11.17 - [Python/Tip & Etc] - python Thread(쓰레드) 병렬처리

 

1. 문제 다시보기

 Question.

A선수와 B 선수가 100m 달리기를 한다. 두 선수는 컨디션에 따라 결과를 예측하기 어려운 라이벌로 A 선수는 평균 4~7m/s, B선수는 평균 5~6m/s의 기록을 가지고 있다.

오늘 두 선수의 속도는 최소값과 최대 값 사이의 임의의 정수 값을 가질 때, 결과를 출력하는 코드를 작성해보세요. 

 

 

결과 풀이에 앞서 지난 포스팅에서는 매서드를 이용한 방법으로 쓰레드를 처리하였는데,

앞선 포스팅과 같이 함수를 사용할 수도 있고, 클래스로도 사용할 수 있다.

 

오늘은 이 2가지를 모두 살펴보겠다.

 

2. 매서드(함수)로 해결하기

 - 함수를 동시에 처리하기 위해 threading 라이브러리를 사용하고, 

 - 경기일 컨디션에 따른 속도를 임의로 지정하기 위해 random 라이브러리를 사용한다.

 - 1초가 경과할 때마다 진행 상황을 보여주기 위해 time 라이브러리까지 준비하면 준비는 끝이난다.

 - finish 라는 전역변수를 만들어 경기가 끝이 났는지(1등이 결승선을 지나쳤는지) 확인을 하고,

 - 1등이 결정되면 해당 선수의 이름을 finish_player 변수 값에 넣는다.

import threading, time, random
finish = False
finish_player = ''

def a_player(speed):
    global finish
    global finish_player
    
    run_distance = 0 
    lap_time = 0
    print('A 선수 Start')
    while run_distance < 100 and finish == False:
        lap_time += 1
        time.sleep(1)
        run_distance += speed
        print(f'{lap_time}초 : A 선수가 {run_distance}m 지점을 지나갑니다.')

    print(f'A 선수가 결승선에 도착했습니다.')    
    if finish == False:
        finish = True
        global finish_player
        finish_player = 'A 선수'
    


def b_player(speed):
    global finish
    global finish_player
    
    run_distance = 0 
    lap_time = 0
    print('B 선수 Start')
    while run_distance < 100 and finish == False:
        lap_time += 1
        time.sleep(1)
        run_distance += speed
        print(f'{lap_time}초 : B 선수가 {run_distance}m 지점을 지나갑니다.')

    print(f'B 선수가 결승선에 도착했습니다.')    
    if finish == False:
        finish = True
        global finish_player
        finish_player = 'B 선수'
    

def congratulation():
    global finish_player
    print('축하합니다.')
    print(f'우승자는 {finish_player} 선수입니다.') 


ran_a_speed = random.randint(4,7) # A 선수의 임의 값 속도
thread_a = threading.Thread(target=a_player, args=(ran_a_speed,))
thread_a.daemon = True

ran_b_speed = random.randint(5,6) # B 선수의 임의 값 속도
thread_b = threading.Thread(target=b_player, args=(ran_b_speed,))
thread_b.daemon = True


thread_a.start()
thread_b.start()

thread_a.join()
thread_b.join()


congratulation()

 

 - 경기 결과는 실행 때마다 달라질 수 있다.

 - 경기 결과를 반복하다보면, 두 선수의 속도가 같은 값이 될 때가 있는데, 

   속도가 같으면 무승부가 나야 하지만, 결과 값을 보면 같은 20초에 100m 지점에 도착했지만

  특정 선수가 우승자라고 나타나는 것을 볼 수 있다.

 

 - GIL(Global Interpreter Lock) 이라는 제어 방법에 따라 생기는데, 

    데이터의 일관성 있는 결과(메모리의 안정성)을 유지하기 위해 GIL이 사용되지만 이로 인해

    쓰레드를 사용하더라도 완전히 2가지가 동시에 진행되지 않음을 뜻한다. 

 

 

3. 클래스(class)로 해결하기

  - 위에 코드에서 def a_player 와 def b_player를 비교해보면 거의 모든 내용이 유사하다.

  - 이럴 때는 코드의 재사용을 위해 클래스(class)를 사용하는 방법이 좋은데,

    Player_info 클래스를 생성할 때 threading.Thread 클래스를 상속 받으면 된다.

 

 - 가장 핵심적인 부분은 Player_info 클래스 내의 run 함수인데,

   오버라이딩이라는 기능을 사용하기 위해 꼭 run이라는 매서드로 이름을 생성해야 한다.

 - 오버라이딩(overriding)이란 기반 클래스(부모 클래스 / threading.Thread)의 매서드를 무시하기 위해, 

   동일한 이름을 가진 매서드를 자식클래스(Player_info)에 생성하는 것을 말하는데,

   이렇게 오버라이딩을 하게되면 run을 실행하더라도 부모클래스(threading.Thread)의 run 매서드가 실행되는 것이 아닌

   자식클래스(Player_info)의 run이 실행되게 된다.

 

  - 생성된 a_palyer(56라인)와 b_player(59라인) 객체는 기존의 매서드(함수) 방식과 달리 threading.Thread를 상속 받고 있기 때문에 해당 객체에 start()와 join() 매서드를 실행하더로도 동작하는 것을 확인할 수 있다.

  - 이때 동작을 하게 되는 start() 매서드는 run() 매서드를 실행하게 되는데, 

    우리가 오버라이딩으로 run 매서드를 새로 정의했기 때문에 Player_info의 run 매서드를 사용하게 된다.

import random, time, threading

finish = False
finish_player = ''
all_player = []


class Player_info(threading.Thread): #threading.Thread 상속



    def __init__(self, player_name, speed_range):
        threading.Thread.__init__(self)

        self.player_name = player_name # 선수 이름
        global all_player
        all_player.append(self.player_name)
        self.speed_range = speed_range # 선수 속도 범위
        self.player_goalin = False # 결승선 통과 Flag
        


    def set_today_speed(self):
        self.player_goalin = False # 결승선 통과 Flag
        self.today_speed = random.randint(int(self.speed_range[0]), int(self.speed_range[1]))
        
        #return(str(self.today_speed()))
        
    def run(self): # threading.Thread 의 run 매서드 오버라이딩
 
        self.run_distance = 0 
        self.lap_time = 0
        print(f'{self.player_name} 선수 Start')
        while self.run_distance < 100 and self.player_goalin == False:
            self.lap_time += 1
            time.sleep(1)
            self.run_distance += self.today_speed
            print(f'{self.lap_time}초 : {self.player_name} 선수가 {self.run_distance}m 지점을 지나갑니다.')

        print(f'{self.player_name} 선수가 결승선에 도착했습니다.') 
        
        global finish
        global finish_player
        if finish == False:            
            finish = True
            finish_player = self.player_name   

        
def congratulation():
    global finish_player
    print('축하합니다.')
    print(f'우승자는 {finish_player} 선수입니다.') 



a_player = Player_info('홍길동',(4,7))
a_player.set_today_speed()

b_player = Player_info('전우치',(5,6))
b_player.set_today_speed()




a_player.start()
b_player.start()

a_player.join()
b_player.join()

congratulation()
반응형

 

4. 승률 확인하기

 - 10번의 반복을 통해 어느 선수의 승률이 더 높은지 확인해보겠습니다.

 - 사실 ranint로 생성되는 선수별 set_today_speed 만 확인하여도 결과는 정해지는 걸테지만,

   재미삼아 진행해보겠습니다.

   ( 실제 테스트간 두 선수가 같은 속도 일 때, 5 또는 6일때 무승부가 아닌 먼저 처리되는 서브쓰레드의 선수가 승리로 반영되어(위에 언급된 GIL.....로 인해) 해당 속도가 같을 때는 제외 하고 진행하겠습니다.)

  - 위의 코드에서 반복문을 진행하는 동안 몇가지 변수 값만 초기화를 진행하고 결과를 확인해보겠습니다.

  - 한번 생성된 Thread 객체는 start를 1회 밖에 진행할 수 없기 때문에 a_player, b_player를 생성하는 객체는 while문 안에 포함되었습니다.

  ( *해당 코드가 while 문 보다 상위에 작성되면 2회째 실행을 할 때, 'threads can only be started once' 라는 Error 가 발생합니다.)

from tkinter import *
from tkinter import ttk
import random, time, threading

finish = False
finish_player = ''
all_player = []


class Player_info(threading.Thread):



    def __init__(self, player_name, speed_range):
        threading.Thread.__init__(self)

        self.player_name = player_name # 선수 이름
        global all_player
        all_player.append(self.player_name)
        self.speed_range = speed_range # 선수 속도 범위
        self.player_goalin = False # 결승선 통과 Flag
        


    def set_today_speed(self):
        self.player_goalin = False # 결승선 통과 Flag
        self.today_speed = random.randint(int(self.speed_range[0]), int(self.speed_range[1]))
        
        return(str(self.today_speed))
    def run(self):
 
        self.run_distance = 0 
        self.lap_time = 0
        print(f'{self.player_name} 선수 Start')
        while self.run_distance < 100 and self.player_goalin == False:
            self.lap_time += 1
            time.sleep(1)
            self.run_distance += self.today_speed
            print(f'{self.lap_time}초 : {self.player_name} 선수가 {self.run_distance}m 지점을 지나갑니다.')

        print(f'{self.player_name} 선수가 결승선에 도착했습니다.') 
        
        global finish
        global finish_player
        if finish == False:            
            finish = True
            finish_player = self.player_name   

        
def congratulation():
    global finish_player
    print('축하합니다.')
    print(f'우승자는 {finish_player} 선수입니다.') 



match_count = 0
win_a = 0 # A 선수(홍길동)의 승리 횟수
win_b = 0 # B 선수(전우치)의 승리 횟수

while match_count < 10:

    finish = False # 경기 초기화
    
    a_player = Player_info('홍길동',(4,7))
    b_player = Player_info('전우치',(5,6))


    today_a = a_player.set_today_speed() # 해당 경기일 속도 초기화
    today_b = b_player.set_today_speed() # 해당 경기일 속도 초기화

    #print(f'{today_a} : {today_b}')

    if today_a != today_b:
        print(f'{match_count+1}번째 경기를 시작합니다.')
        
        a_player.start()
        b_player.start()

        a_player.join()
        b_player.join()

        congratulation()
        if finish_player == '홍길동':
            win_a += 1
        elif finish_player == '전우치':
            win_b += 1
        

        match_count += 1
        print('경기가 종료되었습니다. \n\n')


print(f'홍길동선수와 전우치선수의 승률은 {win_a} 대 {win_b} 입니다.')

 - 결과는 그날의 컨디션에 따라 기복은 있으나 최대 속도가 더 뛰어난 홍길동 선수보다, 

   기복 없이 일정한 역량을 발휘하는 전우치 선수가 더 높은 승률을 보이는 것으로 결과가 마무리되었습니다.

 

 - 어떠신가요? 파이썬을 배웠을 뿐인대, 이런 결과를 도출해낼 수 있다는 것도 신기하지 않으신가요?

 

 - 파이썬은 도구일뿐이라는 말도 어느정도 체감이 되지는 않았을까라는 생각을 갖게되는 오늘의 포스팅입니다.

반응형