본문 바로가기
wecode/TIL 정리

위코드 Pre Course - 파이썬의 Thread

by 왕거 2020. 7. 25.

파이썬에서 하나의 실행 단위라고 할 수 있는 Thread를 정리한다.

 

Thread란?

  • 스레드라고 읽는다.
  • 병렬처리와 관련된 기본적인 개념과 동일한 의미를 가진다.
  • 장점
    • 적절하게 구현된 스레드 구조는 시스템의 자원을 더 효율적으로 사용해서 빠르고 효과적으로 로직을 처리 할 수 있다.
    • 폴링 방식으로 작동하는 로직이 있을 경우에 스레드를 사용해서 비동기적으로 작업을 진행할 수 있다.
  • 단점
    • 무분별하게 구현된 스레드는 시스템의 자원적인 측면에 무리를 줄 수 있다.
    • 스레드 간 자원공유에 대한 명확한 정책을 세워놓지 않으면 설계와 다른 방향으로 동작할 수 있다.

Assignment

  • 2개의 스레드에서 1억까지 1씩 더해가는 프로그램의 작동 결과 확인과 문제점 파악
# Assignment 1
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")
    
#################################################################
# 실행 결과
number = 50000000
number = 50000000
--- 12.252983570098877 seconds ---
shared_number=53298096
end of main
  • 예상한대로 동작했다면 shared_number의 값은 100000000이 출력되어야 하나, 그에 한참 못 미치는 값을 반환하였다.
    • 스레드 간 공유 자원에 대한 중요성에 대한 예제라고 볼 수 있다.
    • 스레드가 완전 동시에 로직을 처리하지는 않는다. 조금씩 차이가 발생하는데 발생한 상황은 다음과 같다.
      • t1 스레드가 shared_number 변수를 읽음, t2 스레드가 결과 값을 shared_number 변수에 저장
        • 이 상황에서는 t1 스레드가 작업을 완료했을 때 t2 스레드의 작업 내용을 덮어 쓰게 된다.
        • 반대의 경우도 동일함
    • 문제 해결을 위해 필요한 조건은 공유되는 부분을 어떻게 처리하느냐에 따라서 2가지의 해결방법이 있다.
      1. 스레드가 공유하는 자원에 대해서 접근할 때 해당 자원이 사용중인지 아닌지에 따라서 한 스레드만 접근이 가능하게 만든다.
      2. 스레드 간에 공유하는 자원을 없애고 각 스레드 별 고유의 자원을 할당 한 후에 마지막으로 합치는 과정을 수행한다.
# 각 스레드에 고유한 자원을 할당하는 방법
import threading
import time

t1_number = 0
t2_number = 0

def thread_1(number):
    global t1_number
    print("number = {}".format(number))
    
    for i in range(number):
        t1_number += 1

def thread_2(number):
    global t2_number
    print("number = {}".format(number))
    for i in range(number):
        t2_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("result number = {}".format(t1_number + t2_number))
    print("end of main")
    
#################################################################
# 실행 결과
number = 50000000
number = 50000000
--- 12.311098098754883 seconds ---
result number = 100000000
end of main
# 공유 자원에 대한 접근을 하나의 스레드로 제한
import threading
import time

shared_number = 0
lock = threading.Lock()

def thread_1(number):
    global shared_number
    print("number = {}".format(number))
    
    for i in range(number):
        lock.acquire()
        shared_number += 1
        lock.release()

def thread_2(number):
    global shared_number
    print("number = {}".format(number))
    for i in range(number):
        lock.acquire()
        shared_number += 1
        lock.release()


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")
    
###########################################################################
# 실행 결과
number = 50000000
number = 50000000
--- 63.83968925476074 seconds ---		#실행 시간이 너무하다!!
shared_number=100000000		
end of main
  • 공유 자원에 대한 접근을 제한하는 방법은 여러가지가 있지만 뮤텍스와 조건변수에 대해서 정리해보자.

뮤텍스란?

  • 뮤텍스(MutEx)란 상호배제(Mutual Exclusion)의 약자이다.
    • 파이썬만의 개념이 아님!
  • 임계구역(Critical Section)
    • 각각의 프로세스 또는 스레드등의 접근 단위가 동시에 접근하면 안되는 공유 영역을 뜻함
      • 주어진 코드에서는 Shared_Number 변수가 이에 해당한다.
      • 두 스레드의 실행 결과가 100000000이 아닌 상황을 임계 구역 문제라고 말함
    • 임계 구역을 시작하는 코드를 시작 구역(Entry Section), 임계 구역을 빠져나가는 코드를 퇴장 구역(Exit Section)이라고 부른다.
    • 임계 구역이 아닌 부분은 나머지 구역(Remainder Section)이라고 부름
  • 임계 구역 문제를 해결하기 위한 방법이 뮤텍스
    • Lock과 Unlock로 구성되며 파이썬의 경우는 뮤텍스 객체를 만들어 준 후에 acquire() 메소드로 Lock을 걸고, release() 메소드로 Unlcok을 수행한다.
# 뮤텍스 예제
import threading
import time

shared_number = 0
lock = threading.Lock()	#뮤텍스 객체를 만든다.

def thread_1(number):
    global shared_number
    print("number = {}".format(number))
    
    for i in range(number):
        lock.acquire()	# 진입 구역 - Lock 수행 
        shared_number += 1
        lock.release()	# 탈출 구역 - Unlock 수행
  • 임계 구역 문제를 해결하기위한 뮤텍스라고 하지만 절대적으로 문제를 방지할 수 있는것이 아님!
    • 추가적인 로직을 더 적용함으로써 교착 상태가 발생할 가능성을 줄일 수 있다.
  • 그렇다면 뮤텍스를 사용한 방법의 실행 시간이 길어진 이유는 무엇일까?
    • 이건 코드의 문제로 각 스레드의 로직은 주어진 숫자에 도달할 때까지 0부터 계속 반복해서 1을 누적하는 로직이다.
    • 이 로직에 뮤텍스를 적용하면 발생하는 상황은 다음과 같다.
      • 임계구역의 Lock 상황을 확인
      • Unlock 상황이라면 Lock 후 로직 수행
        • 로직 수행 후 Unlock 수행
      • Lock 상황이라면 Unlock까지 대기
        • 이후 Unlock되면 Lock 수행 후 로직 실행
        • 로직 수행 후 Unlock 수행
    • Lock을 확인하고 Lock을 수행하고, Unlock을 수행하고, Unlock 될 때까지 대기하는 등의 작업은 모두 자원을 소모하는 작업이며 1을 누적하는 단순하지만 양이 많은 작업을 처리하는 로직에 적용하는 것은 쓸데 없이 많은 일을 하는 느낌이다.
    • 단순하게 스레드를 쓰지 않은 로직과 비교해도 4배 정도의 실행시간의 차이가 난다.
    • 이 예제로 배울 수 있는 점은?
      • 스레드를 사용하고 뮤텍스를 적용하는 방법이 무조건 좋은 결과를 내지는 않는다.
      • 보다 효율적인 설계와 데이터 흐름을 확정한 후 그에 맞는 스레드 구조와 뮤텍스를 사용해야만 의미있는 성능의 향상을 기대할 수 있다.

번외편. 세마포어(Semaphore)

  • 뮤텍스와 비슷한 목적으로 사용되고 동일한 상호 배제의 원칙에 따른다.
  • 뮤텍스와 세마포어의 차이점을 간단하게 비교하면
    • 단일 공유 자원에 대한 접근 제어 - 뮤텍스
    • 여러 공유 자원에 대한 접근 제어 - 세마포어
  • 뮤텍스의 경우는 뮤텍스를 사용하는 스레드에서만 unlock을 수행할 수 있지만 세마포어의 경우 다른 스레드에서도 세마포어를 제어할 수 있다.
  • 세마포어 역시 파이썬 고유의 개념이 아님!
    • 차후에 더 확실하게 정리가 필요하다.