Python

[Python] Thread

UnoCode 2020. 6. 29. 02:01

스레드란?

 

스레드는 프로그램이 실행되는 실행 흐름의 초소 단위입니다.

 

우리가 어떠한 windows기준 exe파일을 실행되면 기본적으로 해당 프로그램을 위한 프로세스가 생성됩니다.

 

그리고 다시 이 프로세스는 하나의 스레드를 만들고 이 스레드를 따라 코드가 실행됩니다.

 

스레드는 프로세스에 종속되므로 프로세스 내에서 스레드가 추가로 만들어질 때 새로운 스레드는 프로세스 코드와 메모

 

리를 공유 합니다. m.blog.naver.com/fortbank/220654857574

 

https://www.geeksforgeeks.org/multithreading-python-set-1/

파이썬은 스레드를 사용할 수 있도록 threading모듈을 제공하고 있습니다.

 

사용법은

 

import threading 

 

스레드 만들기

스레드 객체의 생성법

 

Thread(name=, target=, args=, kargs=, *, daemon=)

인자

 

  • name : 스레드의 이름. 로깅들을 위한 용도로 쓰며 주지 않아도 무방하다.
  • target : 스레드에서 실행할 함수
  • args : target에 넘겨질 인자 (듀플 형식)
  • largs : target에 키워드 인자 (딕셔너리 형식)
  • deamon : 데몬 실행 여부, 데몬으로 실행되는 스레드는 프로세스가 종료될 때 즉각 중단된다.

속성

 

스레드 객체를 생성하더라도 바로 실행되지 않고 start()를 호출하면서 시작된다. 그외 스레드는 여러개의 스레드 객체를 가지고 있다. (docs.python.org/ko/3/library/threading.html#thread-objectsdocs.python.org/ko/3/library/threading.html#threading.Thread.start 참조)

 

  • start() : 스레드 시작
  • join() : 해당 스레드에서 실행되는 함수가 종료될때까지 기다린다.

              timeout= 인자를 주어 특정 시간까지만 기다리게 할 수 있다.

              타임아웃을 초과해도 예외를 일으키지 않고 None을 리턴하므로 이 경우 is_alive() 호출하여 스레드가 실행

              중인지를 파악할 필요가 있다.

 

  • is_alive() : 해당 스레드가 동작 중인지 확인한다.
  • name : 스레드의 이름
  • ident : 스레드 식별자. 정수값
  • native_id : 스레드 고유 식별자 ident 는 종료된 스레드 이후에 새로 만들어진 다른 스레드에 재활용될 수 있다.
  • deamon : 데몬 스레드 여부

샘플 코드

import threading 
# import logging
# import time

def job():
    while 1:
        print("첫 번째 일")

def job2():
    while 1:
        print("두번째 일")

job()
job2()

# work = threading.Thread(target=job)
# work2 = threading.Thread(target=job2)

# work.start()
# work2.start()

 

무한루프를 도는 2개의 함수가있다.

 

우리가 쓰레드를 지정하지 않고 job을 호출시키면

 

job1만 실행할 것이다.

 

Why 코드는 순서대로 진행되는데 job1이 무한로프 도니까...

 

이것을 쓰레드를 이용해서 동시에 무한루프가 2개 돌도록 만들어 보겠습니다.

import threading 
# import logging
# import time

def job():
    while 1:
        print("첫 번째 일")

def job2():
    while 1:
        print("두번째 일")

work = threading.Thread(target=job)
work2 = threading.Thread(target=job2)

work.start()
work2.start()

무한루프 2개가 반복적으로 이용하는걸 볼 수 있습니다.

 

이것이 쓰레드가 하는 일입니다.

 

쓰레드 사용시 주의할 점

 

스레드를 사용 할 때 주의할 점은 Thread.start()는 작업이 끝나면 즉시 종료되기 때문에 스레드들이 동작하고

있는 중일 때 메인 스레드가 적절히 기다려 주지 않는다면 프로그램이 중간에 끝나버릴 수도 있다.

 

프로세스의 종료 시점은 메인 스레드가 종료 지점에 도달했을 때이며 다른 스레드의 실행여부는 교려되지 않는다.

 

이를 막기위해 .join 메소드가 사용된다.

 

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

    print("end of thread_1",shared_number)

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

    print("end of thread_2",shared_number)
    

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

위 코드를 실행 시키면 아래 그림과 같이 실행결과가 뜨는데요

 

 

Why shared_number이 천만으로 증가 되지 않는 이유는 무었일까요?

 

스레드는 프로세스 내의 자원을 모두 공유하고 있습니다. 따라서 어떤 스레드에 있는 참견을 받지 않고 자유럽게 공유

되는 개념입니다.

 

이것은 장점도 될수 있고 단점도 될수 있습니다. 매우 편리한 동시에 위험하지요

아무런 안전장치없이 두 개 이상의 스레드가 같은 자원(변수)을 엑세스 하려는 것은 자원 선점 문제를 일으킬 수 도 있기 때문입니다. 스레드는 생성되고 시작되는 순간 다른 스레드를 신경안쓰는데요. 따라서 두 스레드가 데이터를 교환하려고 하거나 특정 동작을 정해진 순서에 맞춰서 실행하기 위해서는 동기화 라는 개념이 필요합디

 

threading에서 재공되는 동기화 수단은

  • 세파포어
  • 이벤트
  • 컨티션
  • 타이머
  • 베리어

이중 락 과 컨티션 두개만 해보도록 하겠습니다.

 

shared_number 를 천만으로 증가시키는 코드를 보겠습니다.

 

락은 보통 칸이 하나 밖에 없는 화잘실 이다 라고 많이들 하는데요.

말 그대로 자물쇠처럼 이를 선점한 그레드가 락을 획득하면 자물쇠가 잠기는 원리입니다.

이후 접근하는 스레드들은 락이 열릴 때까지 그 앞에서 멈춰기다렸다가 락을 선점한 스레드가 락을 풀면 차례로 획득

헤제를 반복하죠

 

이때 락을 선점하는 함수는 acquire()입니다.

락을 헤체하는 함수는 release()입니다.

 

import threading
import time

shared_number = 0
lock = threading.Lock() # lock선언 열쇠는 하나여야 한다.

def thread_1(number):
    global shared_number
    print("number = ",end=""), print(number)
    
    lock.acquire() # 내가 쓸거양
    for i in range(number):
        shared_number += 1

    lock.release() # 키 반납

    print("end of thread_1",shared_number)

def thread_2(number):
    global shared_number
    print("number = ",end=""), print(number)

    lock.acquire() # 내가 쓸거양
    for i in range(number):
        shared_number += 1

    lock.release() # 키 반납
    print("end of thread_2",shared_number)
    

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

shared_number 결과값을 10000000을 만들었군요..

 

 

 

 

이벤트

 

이벤트는 임의의 출발선에 해당한다. 스레드가 초반에 어떤 처리를 하다가 중간 어느 시점에서 다른 스레드를 기다려야

 

할 때 유용하다.

 

import threading
import time

shared_number = 0
evt = threading.Event() # 이벤트 선언

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

    evt.set() # thread1 끝나면 풀어줌

    print("end of thread_1",shared_number)

def thread_2(number):
    global shared_number
    print("number = ",end=""), print(number)
    evt.wait() # 여기서 기다려 

    for i in range(number):
        shared_number += 1

    print("end of thread_2",shared_number)
    

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

 

Condition

 

 

import threading
import time

shared_number = 0
cv = threading.Condition()

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

    print("end of thread_1",shared_number)
    cv.release()
    
def thread_2(number):
    global shared_number
    print("number = ",end=""), print(number)
    cv.acquire()
    for i in range(number):
        shared_number += 1

    cv.notifyAll()
    cv.release()
    print("end of thread_2",shared_number)
    

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

1. 쓰레드 두개에 키를 각각 준다.

     

 cv.acquire()

 

2. 그러나 스레드1은 늦게 작업하고 싶다. 그래서 wait를 건다

 

cv.wait()

 

3. 스레드1이 wait걸려있는 동안 스레드2의 반복문이 돈다.

 

def thread_2

    for i in range(number):

        shared_number += 1

 

4. 스레드2의 반복문이 다돌면 wait상태를 풀어준다.

 

cv.notifyAll()   ---> 이떄 부터 스레드1이 돌기 시작함

 

5. 스레드2의 락을 풀어준다..

 

cv.release()

 

6, 스레드1의 반복문이 실행된다.

 

def thread_1(number):

    for i in range(number):

        shared_number += 1

 

7. 스레드1의 락을 풀어준다.

 

cv.release()