NullNull

Python 쓰레드 실행하기 본문

카테고리 없음

Python 쓰레드 실행하기

KYBee 2024. 4. 8. 23:34

Python 쓰레드 관련 코드를 작성하다가 예상치 못한 상황을 맞이했다.

 

우선 코드부터 보자

import threading
import time
import random

def sub_task(idx, timeout):
  print("subtask started " + str(idx))
  time.sleep(timeout)  
  print("subtask finished " + str(idx))

def main_thread():
  print("main thread started")

  for i in range(10):
    thread = threading.Thread(target = sub_task, args = (i, random.randint(1, 10)))
    thread.start()
  
  print("main thread finished")

if __name__ == "__main__":
  main_thread()

 

 

원래 내 의도는 메인 쓰레드가 자식 쓰레드의 실행이 끝나면 종료되는 것이었다. 이 코드의 실행 결과는 다음과 같다.

main thread started
subtask started 0
subtask started 1
subtask started 2
subtask started 3
subtask started 4
subtask started 5
subtask started 6
subtask started 7
subtask started 8
subtask started 9
main thread finished
subtask finished 1
subtask finished 2
subtask finished 0
subtask finished 9
subtask finished 6
subtask finished 3
subtask finished 7
subtask finished 5
subtask finished 8
subtask finished 4

 

 

 

아니 왜 자꾸 먼저 죽어… 메인 쓰레드야…

 

이 문제를 해결하고자 구글의 힘을 빌렸다.

 

threading.Thread.join()

import threading
import time
import random

def sub_task(idx, timeout):
  print("subtask started " + str(idx))
  #time.sleep(timeout)  
  print("subtask finished " + str(idx))

def main_thread():
  print("main thread started")

  threads = []

  for i in range(10):
    thread = threading.Thread(target = sub_task, args = (i, random.randint(1, 10)))
    thread.start()
    threads.append(thread)

  for thread in threads:
    thread.join()

  print("main thread finished")

if __name__ == "__main__":
  main_thread()

 

가장 빠르게 해결한 방법은 thread모듈의 Thread.join()을 활용하는 것이었다.

 

thread들을 모아둘 자료구조를 하나 만들고 실행된 쓰레드를 하나씩 append 한다. 이 경우에 실행하면 아래와 같은 결과를 얻는다.

 

main thread started
subtask started 0
subtask started 1
subtask started 2
subtask started 3
subtask started 4
subtask started 5
subtask started 6
subtask started 8
subtask started 7
subtask started 9
subtask finished 4
subtask finished 9
subtask finished 3
subtask finished 8
subtask finished 5
subtask finished 1
subtask finished 2
subtask finished 0
subtask finished 6
subtask finished 7
main thread finished

 

오, 그래도 뭔가 의도한 대로 나오는 것 같다.

 

concurrent.futures.ThreadPoolExecutor.submit()

import concurrent.futures
import time
import random

def sub_task(idx, timeout):
  print("subtask started " + str(idx))
  time.sleep(timeout)  
  print("subtask finished " + str(idx))

def main_thread():
  print("main thread started")

  with concurrent.futures.ThreadPoolExecutor(max_workers=10) as exe:    
    for i in range(10):
      exe.submit(sub_task, i, random.randint(1, 10))

  print("main thread finished")

if __name__ == "__main__":
  main_thread()

 

아래와 같이 다른 모듈을 사용해보기도 했다.

 

concurrent.future 에 있는 ThreadPoolExecutor.submit() 을 활용한 코드이다.

결과는 다음과 같다.

 

main thread started
subtask started 0
subtask started 1
subtask started 2
subtask started 3
subtask started 4
subtask started 5
subtask started 6
subtask started 7
subtask started 8
subtask started 9
subtask finished 1
subtask finished 9
subtask finished 3
subtask finished 5
subtask finished 0
subtask finished 7
subtask finished 2
subtask finished 8
subtask finished 4
subtask finished 6
main thread finished

 

 

이것도 의도한 대로 동작한다.

 

그래서 둘이 뭐가 다를까?

 

일단 의도한 대로 코드를 동작시키는 것은 성공했다. 둘의 차이가 궁금해서 우리의 친구 GPT에게 문의했다.

 

1. threading.Thread.join():

  • 단일 쓰레드 대기: threading.Thread.join()은 단일 쓰레드를 대기시키는 데 사용됩니다. 특정 쓰레드가 종료될 때까지 현재 쓰레드를 차단합니다.
  • 저수준 API: threading.Thread은 저수준 쓰레드 관리를 위한 API를 제공합니다. 개발자는 쓰레드의 생성, 실행, 조율, 종료 등을 직접 제어할 수 있습니다.

2. concurrent.futures 모듈:

  • 고수준 API: concurrent.futures 모듈은 고수준의 추상화를 제공하여 병렬 작업을 보다 쉽게 수행할 수 있습니다. 이 모듈을 사용하면 멀티쓰레딩 및 멀티프로세싱 작업을 추상화된 방식으로 다룰 수 있습니다.
  • Executor 패턴: ThreadPoolExecutor 및 ProcessPoolExecutor는 Executor 패턴을 따르며, 작업을 실행하는 데 필요한 쓰레드나 프로세스를 관리합니다. 이는 작업의 스케일링 및 리소스 관리를 용이하게 합니다.
  • 결과 처리 및 예외 처리: submit(), map() 등의 메서드를 통해 작업을 제출하고 실행할 수 있으며, Future 객체를 반환합니다. Future 객체를 통해 작업의 결과를 처리하고 예외를 관리할 수 있습니다.

장단점:

  • threading.Thread.join():
    • 장점: 저수준의 쓰레드 관리를 제공하여 쓰레드 동작을 더 세밀하게 제어할 수 있습니다.
    • 단점: 다수의 쓰레드를 관리할 때는 코드가 복잡해질 수 있고, 쓰레드 간 동기화 및 조율에 관한 문제를 개발자가 직접 다뤄야 합니다.
  • concurrent.futures 모듈:
    • 장점: 고수준의 API를 제공하여 병렬 작업을 보다 쉽게 수행할 수 있습니다. Executor 패턴을 통해 작업 스케일링 및 리소스 관리가 용이합니다.
    • 단점: 멀티플랫폼 환경에서는 멀티프로세싱을 위해 추가적인 리소스를 사용할 수 있습니다. 또한 일부 특정한 상황에서는 쓰레딩보다 더 느릴 수 있습니다.

 

따라서, threading.Thread.join()은 쓰레드 간의 작업을 더 세밀하게 조율할 때 사용되고, concurrent.futures 모듈은 고수준의 추상화를 통해 병렬 작업을 편리하게 처리할 때 사용됩니다. 선택은 작업의 성격과 요구사항에 따라 달라집니다.

 

결론은 threading.Thread.join()의 경우, 저수준의 관리를 제공하기 때문에 코드를 작성할 때 신경 써야할 부분들이 많아진다. 그러나 그로 인해서 더 세밀한 유즈케이스를 커버할 수 있어보인다.

 

반면에 concurrent.future 모듈은 고수준의 관리를 직접 제공하기 때문에, 코드 작성 시에 더 간편하게 코드를 작성할 수 있다. 그러나 자원이 한정되었다면 더 느리게 동작할 수 있다고 한다.

 

 

요구사항을 분석해보고 적당한 라이브러리를 정해서 써야겠다 ㅎ

Comments