Threading 모듈

이찬·2023년 7월 7일
0

Python

목록 보기
13/17
post-thumbnail

✅ 프로세스

  • 한 프로세스는 매번 하나의 로그인만 처리할 수 있기 때문에 동시에 처리할 수가 없다.
  • 프로세스는 각자가 고유한 메모리 영역을 가지기 때문에 스레드에 비하면 메모리 사용이 늘어난다는 단점이 있지만, 이 방식을 통해 싱글 머신 아키텍처로부터 여러 머신을 사용하는 분산 어플리케이션으로 쉽게 전환 가능
  • 프로세스의 예시는 구글크롬의 탭을 생각하면 된다.
  • 한 탭에 문제가 생겨도 다른 탭에 영향이 없다.

✅ 스레드

  • 한 프로세스 내에서 여러개의 스레드로 작업을 처리하는 것(멀티스레드)
    ex) pycharm에서 추천코드도 보여주고 실행하는 동시에 디버깅하면서 수정하는 것
  • 스레드의 예시로는 윈도우 탭을 생각해보면 된다.
  • 한 탭에 문제가 생기면 모든 탭에 영향을 미쳐 전체가 꺼지게 된다.
  • 컴퓨터에서 동작하고 있는 프로그램을 프로세스라고 한다. 보통 1개의 프로세스는 한 가지 일만 하지만 스레드를 사용하면 한 프로세스 안에서 2가지 또는 그 이상의 일을 동시에 수행할 수 있다.

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

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

threading 모듈

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

Threading 모듈 객체

스레드 생성

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이기 때문에 별도로 지정하지 않으면 메인스레드가 종료되어도 서브스레드는 끝까지 실행된다.

구현 방식

  • 파이썬에서 쓰레드를 실행하기 위해서는, threading 모듈의 threading.Thread() 함수를 호출하여 Thread 객체를 얻은 후 Thread 객체의 start() 메서드를 호출하면 된다. 서브쓰레드는 함수 혹은 메서드를 실행하는데, 일반적인 구현방식으로 (1) 쓰레드가 실행할 함수 혹은 메서드를 작성하거나 또는 (2) threading.Thread 로부터 파생된 파생클래스를 작성하여 사용하는 방식 등이 있다.
  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()

==============================================
threading.Thread 로부터 파생클래스를 만드는 방식은 Thread 클래스를 파생하여 쓰레드가 실행할 run() 메서드를 재정의해서 사용하는 방식이다. Thread 클래스에서 run() 메서드는 쓰레드가 실제 실행하는 메서드이며, start() 메서드는 내부적으로 이 run() 메서드를 호출한다. 예를 들어, 아래 예제(A)는 getHtml() 라는 함수를 사용한 방식인데 이를 예제(B)와 같이 파생클래스를 사용하는 방식으로 바꿔 쓸 수 있다. 예제(B)에서 t.start()는 HtmlGetter 클래스에서 재정의된 run() 메서드를 호출하게 된다.

# 예제(A)
import threading, requests, time
 
def getHtml(url):
    resp = requests.get(url)
    time.sleep(1)
    print(url, len(resp.text), ' chars')
 
t1 = threading.Thread(target=getHtml, args=('http://google.com',))
t1.start()
 
print("### End ###")
# 예제(B)
import threading, requests, time
 
class HtmlGetter (threading.Thread):
    def __init__(self, url):
        threading.Thread.__init__(self) 
        self.url = url
 
    def run(self):
        resp = requests.get(self.url)
        time.sleep(1)
        print(self.url, len(resp.text), ' chars')
 
t = HtmlGetter('http://google.com')
t.start()
 
print("### End ###")

- join 메서드
사용된 스레드가 끝날 때까지 메인 스레드가 먼저 종료되지 않고 기다렸다가 종료되게 하는 메서드

주의점

Thread.start() 는 즉시 리턴하기 때문에 워커 스레드들이 동작하고 있는 중일 때 메인스레드가 적절히 기다려주지 않는다면 프로그램이 중간에 끝날 수 있기 때문에 주의해야 한다. 프로세스의 종료 시점은 메인 스레드가 종료 지점에 도달했을 때이며, 다른 워커 스레드의 실행 여부는 고려되지 않는다. 따라서 중도 종료를 막기 위해서는 join() 메소드를 사용하여 메인 스레드가 워커 스레드들을 기다리도록 하여야 한다.

스레드를 너무 많이 생성하면 성능을 향상시키는데 역효과가 날 수 있다. 심지어 GIL이 없는 구현체에서도 그럴 수 있는데 그것은 스레드가 많으면 컨텍스트 스위치에 많은 비용이 들기 때문이다.

데몬 쓰레드

  • 데몬 : 사용자가 직접적으로 제어하지 않고, 백그라운드에서 돌면서 여러 작업을 하는 프로그램

  • 쓰레드를 데몬으로 만들어서 사용할 수도 있다.
    주요 작업을 하는 메인 쓰레드가 따로 있고, 백그라운드 작업을 위한 쓰레드를 띄울 수 있다는 것이다. 이러한 백그라운드 작업을 데몬으로 띄우지 않으면, 백그라운드 작업을 하는 쓰레드가 종료될때까지 메인 프로그램도 종료되지 않는다. 프로그램의 상태를 모니터링 하는 프로그램이 내부에서 주기적으로 동작하는데, 만약 이 프로그램 때문에 메인 프로그램이 종료되지 못하면 이상하다. 데몬쓰레드는 메인 프로그램이 종료될때 자동으로 같이 종료된다.

  • 즉, 정기적이고 부수적인 작업은 데몬으로, 필수적인 메인 작업은 일반 쓰레드로 띄운다.

import threading, requests, time
 
def getHtml(url):
    resp = requests.get(url)
    time.sleep(1)
    print(url, len(resp.text), ' chars')
 
# 데몬 쓰레드
t1 = threading.Thread(target=getHtml, args=('http://google.com',))
t1.daemon = True 
t1.start()
 
print("### End ###")
  • Thread 객체의 daemon 속성을 True로 설정한 후 start() 하면, 해당 서브쓰레드는 데몬 쓰레드가 되고 아래와 같이 메인 쓰레드가 곧바로 종료되면 getHtml 메서드를 마저 실행하지 못하고 바로 데몬 쓰레드를 종료하게 된다. daemon 속성은 디폴트로 False 이므로 별도로 지정하지 않으면 메인 쓰레드가 종료되어도 서브쓰레드는 끝까지 작업을 수행한다.
profile
Kyunghee univ. IE 21

0개의 댓글