thread

백승찬·2020년 10월 26일
0

python

목록 보기
8/10

쓰레드

프로그램의 실행 흐름

프로레스가 부여된 자원을 이용해서 같은 프로세스 내에서 여러 쓰레드들 끼리 자원 공유

쓰레드 작성 예시

import time

if __name__ == "__main__":
    
    increased_num = 0

    start_time = time.time()
    for i in range(100000000):
        increased_num += 1

    print("--- %s seconds ---" % (time.time() - start_time))

    print("increased_num=",end=""), print(increased_num)
    print("end of main")

위의 프로그램은 단일 프로세스의 단일 쓰레드로 일억번 +1을 하는 프로그램

실행 성능 : 6~7초

두개의 쓰레드 예시

import threading
import time

shared_number = 0

def thread_1(number):
    global shared_number
    print("number = ",end=""), print(number)
    
    for i in range(number):
        shared_number += 1

def thread_2(number):
    global shared_number
    print("number = ",end=""), print(number)
    for i in range(number):
        shared_number += 1


if __name__ == "__main__":

    threads = [ ]

    start_time = time.time()
    t1 = threading.Thread( target= thread_1, args=(50000000,) )
    t1.start()
    threads.append(t1)

    t2 = threading.Thread( target= thread_2, args=(50000000,) )
    t2.start()
    threads.append(t2)


    for t in threads:
        t.join()

    print("--- %s seconds ---" % (time.time() - start_time))

    print("shared_number=",end=""), print(shared_number)
    print("end of main")
    

Thread의 장점

  • CPU 사용률 향상
  • 효율적인 자원 활용 및 응답성 향상
  • 코드 간결 및 유지보수성 향상

Thread - 구조

사용법

  • threading 모듈 이용. import threading
    threading에서 내장 모듈인 Thread를 상속 받음

  • threading.Thread 클래스를 상속받는 클래스를 만들어서 run() gkdu rorcp todtjd
    Thread를 구동하기 위해서는 함수명을 run으로 해야함
    MyThread로 생성된 객체를 start() 메소드를 실행할 때 run 메소드가 자동으로 수행

  • thread 객체 생성 후 start() 메소드를 통해 thread 객체 실행

  • target은 스레드로 돌릴 함수, args는 입력 인자.

  • join() 메소드를 통해 스레드가 끝날 때까지 기다림

run 메소드 구현 시

예제 코드

 import threading
  ## threading 모듈을 이용
  ​
  ## threading.Thread를 상속받는 클래스를 만들어서 run하여 객체를 생성한다.
  class Messenger(threading.Thread):
      
      def run(self):
          for _ in range(10):
              ## 변수를 지정하지 않고 단순 반복을 하고 싶을 때는 _언더바를 사용한다.
              print(threading.currentThread().getName())
              
  ​
  send = Messenger(name="Sending out Messages")
  receive = Messenger(name="Receiving Messages")
  ​
  send.start()
  receive.start()

출력 결과

run 메소드 없이 스레드 구현

 from threading import Thread
  import time
  ​
  def my_thread(val):
      for i in range(3):
          print("I'm Thread")
          time.sleep(2)
  ​
  ​
  ## 인스턴스 만들기
  ## 첫번째 argu는 스레드 함수 이름, 두번째 argu는 매개변수를 튜플 형태로 전달한 것.
  t1 = Thread(target = my_thread, args=(1,))
  ​
  ## 스레드 시작.
  t1.start()
  ​
  ## main
  for i in range(3):
      print("I'm main Thread")
      time.sleep(1)
  print("----the end----")

실행 결과

join() 메소드 추가하여 실행

 from threading import Thread
  import time
  
  def my_thread(val):
      for i in range(3):
          print("I'm Thread")
          time.sleep(1)
  
  
  ## 인스턴스 만들기
  ## 첫번째 argu는 스레드 함수 이름, 두번째 argu는 매개변수를 튜플 형태로 전달한 것.
  t1 = Thread(target = my_thread, args=(1,))
  
  ## 스레드 시작.
  t1.start()
  t1.join()
  
  ## main
  for i in range(3):
      print("I'm main Thread")
      time.sleep(1)
  print("----the end----")

출력 결과

join() 메소드를 main함수전에 써주면 자식 스레드가 다 출력된 후 메인이 출력되는 것을 확인 할 수 있다.

shared_number가 천만으로 증가되지 않는 이유

  • Python은 하나의 Thread(main_Thread)로 시작
  • main_thread는 혼자서 순차적으로 코드를 실행 but 실행되던 중간에 Blocking Function
    ex) input과 같은 함수를 만나면 그 함수의 실행이 끝날 때까지 기다리게 된다.
  • main_thread가 멈추게 되면 다른 함수를 실행할 수가 없게 된다. 이때 사용자가 Thread를 하나 더 만들어서 다른 함수를 병렬적으로 Blocking Function과 같은 함수와 함께 실행

Thread 사용 시 주의 할 점

  1. 전역 변수를 공유한다

    문제 예시

 import threading
 
# CounterThread
class CounterThread(threading.Thread):
    def __init__(self):
        threading.Thread.__init__(self, name='Timer Thread')
 
    # CounterThread가 실행하는 함수
    def run(self):
        global totalCount
 
        # 2,500,000번 카운트 시작
        for _ in range(2500000):
            totalCount += 1
        print('2,500,000번 카운팅 끝!')
 
if __name__ == "__main__":
    # 전역 변수로 totalCount를 선언
    global totalCount
    totalCount = 0
 
    # totalCount를 1씩 더하는 
    # Counter Thread를 4개 만들어서 동작시킨다.
    for _ in range(4):
        timerThread = CounterThread()
        timerThread.start()
 
    print('모든 Thread들이 종료될 때까지 기다린다.')
    mainThread = threading.currentThread()
    for thread in threading.enumerate():
        # Main Thread를 제외한 모든 Thread들이 
        # 카운팅을 완료하고 끝날 때 까지 기다린다.
        if thread is not mainThread:
            thread.join()
 
    print('totalCount = ' + str(totalCount))

실행 결과

위의 실행 결과 totalCount 변수안에 10,000,000이 아니라 4,827,031이 들어가 있다는 것을 확인

문제의 발생 = 같은 변수를 동시에 접근 했기 때문

ex)
a = a + 1은 보통 3가지의 명령어로 진행 그리고 cpu는 명령어를 하나씩 실행

  1. a의 값을 메모리에서 레지스터로 불러온다.
  2. 레지스터에서 더한다.
  3. 더한 값을 실제로 a가 있는 메모리에 저장한다.

a의 진짜 메모리에 1이 더해지려면 3번째 명령어까지 실행 되어야 한다.
하지만 이 3가지 명령어을 4개의 thread가 거의 동시에 실행하려다 보면 한 thread에서 3번째 명령어가 다 끝나기 전에 또 다른 thread에서 덧셈을 시작하게 된다.

이러한 현상 때문에 여러 thread에서 a에 1씩 더했다 하더라도 최종적으로 더해진 변수의 값을 살펴보면 더 작아져서 나오게 된다.

더 쉬운 예시

전역 변수 a가 0으로 초기화되어 있을 때, thread A와 thread B가 서로 동시에 a=a+1을 실행하려고 한다

이 때 CPU동시에 실행되는 것처럼 보이게 하려고 각각의 Thread명령어번갈아 가면서 실행

  • CPU는 Thread A의 1번 명령어를 실행 (a가 0이므로 레지스터 A에 값은 0)
  • 그다음 Thread B의 1번 명령어를 실행 (아직 a가 0이므로 레지스터 B에 들어간 값은 0)
  • 그다음 Thread A의 2번 명령어를 실행 (레지스터 A의 값은 1)
  • 그다음 Thread B의 2번 명령어를 실행 (레지스터 B의 값은 1)
  • 그다음 Thread A의 3번 명령어를 실행 (레지스터 A의 값을 a에 저장 그래서 a =1 로 변한다.)
  • 그다음 Thread B의 3번 명령어를 실행 (레지스터 B의 값을 a에 저장 그래서 다시 a =1으로 유지 된다.)

shared_number가 천만으로 증가되지 않는 이유는 같은 전역 변수를 공유하기 때문에 서로 다른 스레드가 같은 value를 연산하기 때문

값 증가를 위한 해결법

Thread를 동기화 한다

동기화 하는 방법 중에 lock을 이용

Lock => 특정 thread에서 변수를 사용하기 때문에 다른 thread가 사용하지 못 하도록 막는 역할

  • 마치 변수를 잠구는 것 같아 Lock이라 부른다
  • 변수를 다 사용했으면 thread 변수에 대한 Lock을 풀어줘야하는 데 이를 Release()로 사용

Lock 사용법

  1. Lock.aquire() => 잠금 - 다른 Thread은 접근을 못 하게 막는다.
  2. 여기 안에 있는 code들은 무조건 한 thread에 의해서 순차적으로 실행
  3. Lock.release() => 잠금 해체 -다른 thread들에게 접근 가능하도록 잠금을 푼다.

Lock 사용 예시 코드

import threading
 
# 공유된 변수를 위한 클래스
class ThreadVariable():
    def __init__(self):
        self.lock = threading.Lock()
        self.lockedValue = 0
 
    # 한 Thread만 접근할 수 있도록 설정한다
    def plus(self, value):
        # Lock해서 다른 Thread는 기다리게 만든다.
        self.lock.acquire()
        try:
            self.lockedValue += value
        finally:
            # Lock을 해제해서 다른 Thread도 사용할 수 있도록 만든다.
            self.lock.release()
 
# CounterThread
class CounterThread(threading.Thread):
    def __init__(self):
        threading.Thread.__init__(self, name='Timer Thread')
 
    # CounterThread가 실행하는 함수
    def run(self):
        global totalCount
 
        # 2,500,000번 카운트 시작
        for _ in range(2500000):
            totalCount.plus(1)
        print('2,500,000번 카운팅 끝!')
 
if __name__ == '__main__':
    # 전역 변수로 totalCount를 선언
    global totalCount
    # totalCount를 ThreadVariable 오브젝트로 초기화한다
    totalCount = ThreadVariable()
 
    # totalCount를 1씩 더하는 
    # Counter Thread를 4개 만들어서 동작시킨다.
    for _ in range(4):
        timerThread = CounterThread()
        timerThread.start()
 
    print('모든 Thread들이 종료될 때까지 기다린다.')
    mainThread = threading.currentThread()
    for thread in threading.enumerate():
        # Main Thread를 제외한 모든 Thread들이 
        # 카운팅을 완료하고 끝날 때 까지 기다린다.
        if thread is not mainThread:
            thread.join()
 
    print('totalCount = ' + str(totalCount.lockedValue))

실행 결과

0개의 댓글