[Python] thread

최창현·2022년 1월 6일
post-thumbnail

thread

thread(쓰레드)는 프로그램의 실행흐름이다. 하나의 프로세스 안에서 여러개의 쓰레드를
만들 수 있다. 즉 프로세스가 부여된 자원을 이용해서 같은 프로세스 내에서 여러쓰레드들
끼리 자원을 공유할 수 있다. 쓰레드는 동시성을 위해서 만들어진 개념이다. 하나의
프로세스 안에서 두개이상의 쓰레드를 만들게 되면 두개이상의 쓰레드가 동시에 일을 하게된다.


thread local data

스레드 로컬 데이터는 값이 스레드에 따라 달라지는 데이터이다. 스레드 로컬 데이터를
관리하려면 thread의 인스턴스 local(또는 하위 클래스)를 만들고 여기에 속성을 저장하면 된다.

예시코드

mydata = threading.local()
mydata.x = 1

인스턴스의 값은 별도의 thread에 대해 다르다.
클래스 threading.local은 thread local data를 나타내는 클래스이다.


thread object

thread클래스는 컨트롤의 별도의 스레드에서 실행되는 작업을 나타낸다.
활동을 지정하는 두 가지 방법이 있다. 호출 가능한 개체를 생성자에 전달하거나 run() 하위 클래스에서 method를 재정의하는 것이다. 다른 method(생성자 제외)는 하위 클래스에서 재정의 되어서는 안된다. 스레드 객체가 생성되면 스레드의 start()메서드를 호출하여 활동을 시작한다(run() 과 별도). thread의 활동이 시작되면 thread는 '활성'으로 간주한다. thread가 살아있는지 여부는
is_alive()로 확인할 수 있다. 다른 thread는 thread의 join() method로 호출할 수 있다.
join() method가 호출된 thread는 종료 될 때까지 thread 호출을 차단한다.

thread에는 이름이 있다. 이름은 생성자에 전달되고 name속성을 통해 일거나 변경할 수 있다.


thread의 사용

thread는 다음과 같이 사용한다.

class threading.Thread(group=None, target=None, name=None, args=(), kwargs={}, *, daemon=None)

생성자는 항상 키워드 인자로 호출해야 한다. 인자는 다음과 같다.
group: None이어야한다. ThreadGroup 클래스가 구현될 때 향후 확장을 위해 예약되어 있다.
target: run()메소드에 의해 호출 될 callable 객체다. 기본값은 None이며, 아무것도 호출되지 않는다.
name: thread의 이름이다. 기본적으로 고유한 이름이 <>형식으로 구성되는데, 여기서 N은 작은 십진수를 의미한다.
args: target호출을 위한 인자의 튜플이다. 기본값은().
kwargs: target 호출을 위한 인자의 딕셔너리. 기본값은 {}.
None이 아니면, daemon은 thread가 데몬인지를 명시적으로 설정한다. None(기본값)이면, 데몬 속성은 현재 thread에서 상속된다.


Threading


therad and method

start: thread활동 시작. thread 객체 당 최대 한번 호출되어야하며, 객체의 run() 메소드가
별도의 제어 thread에서 호출되도록 배치한다.
run(): thread의 활동을 표현하는 method. 표준 run() method는 target인자로 객체의 생성자에
전달 된 callable 객체를 호출한다. 인자에 args, kwargs가 있다면 인자에서 각각 취한 위치와
키워드 인자로 호출한다.
join(timeout=None): thread가 종료될 때까지 기다린다. join() 메소드가 호출된 thread가 정상적으로 혹은 처리되지 않은 예외를 통해 종료하거나 선택적 시간제한 초과가 발생할 때까지 호출하는 thread를 블록한다. timeout 인자가 없거나 None이면, thread가 종료도리 때까지 작업이 블록된다. thread는 여러번 join()될 수 있다.
is_alive(): thread가 살아있는지를 반환한다. run() method가 시작되기 직전부터 run() method가 종료된 직후까지 True를 반환한다.
daemon: 해당 thread가 daemon thread인지(true) 아닌지(false)를 나타내는 boolean값.
start()가 호출되기 전에 설정되어야한다. 그렇지 않으면 RuntimeError가 발생한다.
일반 thread는 프로그램이 종료되어도 백그라운드에서 실행되고 있지만, Daemon thread로 설정하면
프로그램이 종료되면 thread도 함께 종료된다.


예제코드는 다음과 같다

if __name__ == "__main__": 
    increased_num = 0 
    start_time = time.time()
    for i in range(10000000):
        increased_num +=1
    print("--- %s seconds ---" % (time.time() - start_time))
    print("increased_num=", end=""), print(increased_num)
    print("end of main")


위의 프로그램은 단일 프로세스의 단일 쓰레드로 일억번 +1을 하는 프로그램이다. 실행시켜보면 컴퓨터
성능에 따라 다르지만 테스트컴퓨터에서는 6~7초 정도가 나오고 증가된 숫자도 1억이 되는것을 확인할 수 있다. 이제 쓰레드를 두개 만들어서 실행해보면 다음과 같다.

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")

파이썬에서 멀티 쓰레드를 사용하려면 threading 모듈을 임포트해서 사용한다.
threading 모듈의 Thread 클래스에 target 인자에 쓰레드 함수를 지정해주고 args로 매게변수를 전달할 수 있다.

위의 코드의 결과는 다음과 같다.


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

위의 질문에 대한 답을 하기 위해선 파이썬의 GIL(Global Interpreter Lock)에 대해서 알아야한다. GIL이란 하나의 thread에서 모든 자원의 점유를 허락하는것이다. 즉, 다른
thread는 자원을 acquire하기 전에는 아예 실행조차 될 수 없다. 파이썬은 정수, 변수 하나까지 모두 객체로 다루기때문에 이렇게 객체마다 lock을 걸어야 하는 작업은 매우 비효율적이기 때문에 파이썬은 인터프리터를 통해 lock을 건 것이다. 따라서 thread 갯수, CPU의 코어와 관계없이 인터프리터를 사용하는 thread는 오직 한개로 만들었다.

GIL의 내용을 토대로 생각해보면, 두개의 CPU가 각각의 thread를 실행시키길 바라면서 두개의 thread를 실행시켰다. 하지만 실제로는 파이썬에서 GIL에 의해서 하나의 CPU에서 두개의 thread를 실행시킨것이기 때문에 성능향상의 결과가 나오지 않는 것이다.


Mutex?

위의 코드 기반으로 두개의 thread에서 각각
50000000씩 증가시켜서 shared_number를 천만으로 증가시키는 코드를 구현해보려 한다.
(with 조건변수, 뮤텍스)

Mutex란 쓰레드들 간에서 공유가 배제되는 객체이다. 파일과 같은 고유 자원이 수행 중 오직 한 프로그램이나 쓰레드에게만 소유되어야 할 필요가 있을 때 그 자원에 대한 뮤텍스 객체를 생성시킨다. 뮤텍스가 비신호 상태이면 프로그램은 자원을 점유하여 사용한 후 이를 반환하고, 다른 프로그램 또는 다른 쓰레드가 자원을 사용중(뮤텍스가 신호대기상태면)대기 상태로 들어가 끝나기를 기다린다. 뮤텍스는 다음과 같은 특징을 가지고 있다.

하나의 쓰레드만 크리티컬 섹션에 들어갈 수 있다.(그 영역에 하나의 쓰레드가 있다면 들어갈 수 없다.)
뮤텍스를 생성하고 난 뒤 파괴하고 싶다면, 반드시 뮤텍스를 unlock하고 파괴해야한다.

뮤텍스는 다음과 같은 순서로 동작한다.

  1. 뮤텍스를 통해 lock을 걸고 푼다.
  2. lock~unlock 부분을 모두 수행할 때 까지 다른 쓰레드가 수행이 되지 않고 wait상태로 있는다. 뮤텍스의 개념으로 구현한 코드는 다음과 같다.
import threading
import time
shared_number = 0
lock = threading.Lock()
def thread_1(number):
    global shared_number
    print("number = ",end=""), print(number)
    for i in range(number):
        lock.acquire()
        shared_number += 1
        lock.release()
def thread_2(number):
    global shared_number
    print("number = ",end=""), print(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")

실행 결과는 다음과 같다.
![](https://velog.velcdn.com/images%2Fchoich_0807%2Fpost%2F4d7ae2fb-8019-416e-bc88-9efa7cf63ac3%2Fimage.png)
시간이 조금 오래 걸리긴 했지만 원하는 출력을 얻을 수 있었다.
시간이 이렇게 오래걸린 이유는 뮤텍스는 Lock을 확인하고, Lock에 관련된 내용을 수행하고, 
Unlock을 확인하고 , Unlock에 관련된 내용을 수행한다.

___
# 조건변수
조건변수는 다른 thread에서 신호를 줄 때까지 기다릴 수 있는 컨디션 변수 객체이다.
조건변수(Condition Variable)는 내부에 thread 대기 큐를 갖고 있다. wait() 메소드가
호출된 thread는 이 대기 큐에 들어가게 되고 sleep상태가 되며, notify()나 notifyAll()
메소드에의해 깨어나게 된다. 다음 예시를 살펴보자

>내가 어떤 방으로 들어갈 수 있는 키(acquire)를 가지고 있다. 그리고 그 방에 들어가서
커피를 다섯잔 마시려고 한다. 방에 막상 들어가니 커피가 세잔밖에 없어서 커피 세잔을 마시고
두잔이 더 만들어지기를 wait()한다. 이제 커피를 만들어 줄 직원이 내가 내려놓은 키(acquire)를 가지고 들어가서 커피를 만든다. 커피를 다 만들면 다 만들었다고 알려주고(notify), 갖고 있던 키를 내려놓는다(release). 그러면 이제 기다리고 있었던 나는 키를 가지고 다시 진행을 하게 되는거다.
profile
chch_oi

0개의 댓글