thread - threading모듈

Nam Eun-Ji·2020년 11월 26일
0

파이썬은 기본적으로 하나의 스레드에서 실행되어, 하나의 메인 스레드에서 파이썬 코드를 순차적으로 실행한다. 코드를 병렬로 실행하기 위해서는 별도의 스레드를 생성하여야 하는데, threading 모듈을 사용하면 된다.

파이썬은 GILGlobal Interpreter Lock 때문에 특정 시점에 하나의 코드만을 실행하게 된다. 다중 CPU에서 파이썬 코드를 병렬로 실행하기 위해서는 다중 프로세스를 이용하는 multiprocessing 모듈을 사용한다.




threading 모듈

파이썬에서 스레드를 실행하기 위해서는, threading 모듈의 threading.Thread() 함수를 호출하여 Thread 객체를 얻은 후 Thread 객체의 start() 메서드를 호출하면 된다.

Threading 모듈 객체

객체설명
Thread단일 실행 스레드를 만드는 객체
Lock기본적인 락 객체
RLock재진입 가능한 락객체. 이미 획득한 락을 다시 획득 할 수 있다.
Condition다른 스레드에서 신호를 줄 때까지 기다릴 수 있는 컨디션 변수 객체
Event컨디션 변수의 일반화 버전.
Semaphore정해놓은 갯수만큼의 스레드를 허용하는 동기화 객체. (예를들어 최대 50개만 동시에 실행)
BoundedSemaphore초기 설정된 값 이상으로 증가 될 수 없게 재한한 Semaphore
TimerThread 와 비슷하지마 실행되기 전에 지정된 시간 동안 대기
Barrier스레드들이 계속 진행 할 수 있으려면 지정된 숫자의 스레드가 해당 지점까지 도달해야하게 만듬 (파이썬 3.2에서 처음 소개됨)


스레드 생성

Thread(name=, target=, args=, kwargs=, *, daemon=)
  • name : 스레드의 이름. 로깅 등을 위한 용도로 쓰며, 주지 않아도 무방.
  • target : 스레드에서 실행할 함수
  • args : target에 넘겨질 인자, tuple 형식으로 넘겨줘야함.
  • kwargs : target에 넘겨질 키워드 인자, dict 형식으로 넘겨줘야함.
  • daemon : 데몬 실행 여부. 데몬으로 실행되는 스레드는 프로세스가 종료될 때 즉각 중단된다.


스레드 클래스 메소드/속성

스레드 객체를 생성했다 하더라도 해당 스레드가 바로 시작되지는 않는다. start()를 호출하면 스레드가 그때부터 시작된다.

  • start()
    • 스레드를 시작한다.
  • join()
    • 해당 스레드에서 실행되는 함수가 종료될 때까지 기다린다.
    • timeout= 인자를 주어 특정 시간까지만 기다리게 할 수 있다.
    • 타임아웃을 초과해도 예외를 일으키지 않고 None을 리턴하므로 이 경우 is_alive()를 호출하여 스레드가 실행 중인지를 파악할 필요가 있다.
  • is_alive()
    • 해당 스레드가 동작 중인지 확인한다.
  • name
    • 스레드의 이름
  • ident
    • 스레드 식별자. 정수값
  • native_id
    • 스레드 고유 식별자. ident는 종료된 스레드 이후에 새로 만들어진 다른 스레드에 재활용될 수 있다.
  • daemon
    • 데몬 스레드 여부 (default = False)
    • 데몬 스레드: 백그라운드에서 실행되는 스레드, 메인스레드가 종료되면 즉시 종료되는 스레드
    • 만약 데몬 스레드가 아니면 해당 서브스레드는 메인스레드가 종료될지라도 자신의 작업이 끝날 때까지 계속 실행된다.
    • default가 False이기 때문에 별도로 지정하지 않으면 메인스레드가 종료되어도 서브스레드는 끝까지 실행된다.


구현 방식

1. 스레드가 실행할 함수 혹은 메서드 작성

스레드가 실행할 함수를 작성하고 그 함수명을 threading.Thread() 함수의 target 아규먼트로 전달하면 된다.
만약 스레드가 실행할 함수에 파라미터를 전달해야 한다면, threading.Thread()의 args(혹은 kwargs)에 지정해주면 된다.
args는 tuple로 파라미터를 전달하고, kwargs는 dict로 전달한다.


아래의 예제를 보면 executeThread함수를 target으로 지정하였고, args에 튜플로 파라미터를 지정해주었다.
주의할 점은 target = executeThread() 처럼 지정하면 이는 executeThread() 함수를 실행하여 리턴한 결과를 target에 지정하는 것이므로 잘못된 결과가 나올 수 있다.

import threading
import urllib.request
import time

def executeThread(i):
    imageName = f"temp/image-{str(i)}.jpg"
    urllib.request.urlretrieve("https://test.com/test.jpg", imageName)

def main():
    t0 = time.time()
    threads = []
    for i in range(10):
        thread = threading.Thread(target=executeThread, args=(i,))
        threads.append(thread)
        thread.start()

    for i in threads:
        print("i ::: ", i)
        i.join()

    t1 = time.time()
    totalTime = t1- t0
    print("Total Execution Time {}".format(totalTime))


if __name__ == "__main__":
    main()

2. threading.Thread로부터 파생된 파생클래스를 작성하여 사용하는 방식

Thread 클래스를 파생하여 쓰레드가 실행할 run() 메서드를 재정의해서 사용하는 방식.
Thread 클래스에서 run() 메서드는 스레드가 실제 실행하는 메서드이며, start() 메서드는 내부적으로 이 run() 메서드를 호출한다.

import 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()


주의점

  • Thread.start() 는 즉시 리턴하기 때문에 워커 스레드들이 동작하고 있는 중일 때 메인스레드가 적절히 기다려주지 않는다면 프로그램이 중간에 끝날 수 있기 때문에 주의해야 한다. 프로세스의 종료 시점은 메인 스레드가 종료 지점에 도달했을 때이며, 다른 워커 스레드의 실행 여부는 고려되지 않는다. 따라서 중도 종료를 막기 위해서는 join() 메소드를 사용하여 메인 스레드가 워커 스레드들을 기다리도록 하여야 한다.
  • 스레드를 너무 많이 생성하면 성능을 향상시키는데 역효과가 날 수 있다. 심지어 GIL이 없는 구현체에서도 그럴 수 있는데 그것은 스레드가 많으면 컨텍스트 스위치에 많은 비용이 들기 때문이다.
profile
한 줄 소개가 자연스러워지는 그날까지

0개의 댓글