하나의 프로세스에서 여러 개의 스레드를 생성하고 제어할 수 있으며 스레드는 프로세스 내부의 흐름의 단위로, 병렬성
을 통해 작업을 처리할 수 있습니다.
Python에서 스레드를 생성하기 위해서는 threading
모듈을 사용합니다. threading.Thread() 함수를 사용하여 스레드 객체를 생성하고,
start()
메서드를 호출하여 스레드를 실행합니다.
또한, join()
메서드를 호출하여 스레드가 종료될 때까지 대기할 수 있습니다.
import threading
def work(s:int, e:int):
for i in range(s, e):
print(f"number : {i}")
print(f"active count : {threading.active_count()}") # 메인 스레드 1개
t1 = threading.Thread(target=work, args=(1, 10))
t1.start()
print(f"active count : {threading.active_count()}") # 메인 스레드와 t1으로 2개
t1.join()
print(f"active count : {threading.active_count()}") # t1 종료로인한 메인 스레드 1개
기본적으로 메인 스레드가 종료되더라도 스레드는 끝까지 동작하지만
메인 스레드가 종료되면 하위 스레드도 종료되게 가능합니다.
import threading
def work(s:int, e:int):
for i in range(s, e):
print(f"number : {i}")
t1 = threading.Thread(target=work, args=(1, 1024))
t1.daemon = True
t1.start()
스레드가 수행할 함수의 인자가 한 개만 필요하더라도
다음과 같이 args에 쉼표
는 유지하여야 합니다.
import threading
def work(e:int):
for i in range(e):
print(f"number : {i}")
t1 = threading.Thread(target=work, args=(10,))
t1.start()
t1.join()
GIL(Global Interpreter Lock)
으로 인해 한 번에 하나의 스레드만 파이썬 코드를 실행할 수 있습니다.
CPU-bound
작업에 대해서는 멀티 프로세스
를 사용하는 것이 효율적이지만
I/O-bound
작업에 대해서는 멀티 스레드
를 사용하는 것이 효율적입니다.
다음과 같이 단일 스레드를 실행해서 수행 시간을 측정하였고 결과는 0.0002초가 소요됐습니다.
import time
import threading
def work(start:int, end:int, total:list):
_ = 0
for i in range(start, end):
_ += i
total.append(_)
def main():
result = []
thread1 = threading.Thread(target=work, args=(0, 1000, result))
start_time = time.time()
thread1.start()
thread1.join()
end_time = time.time()
return end_time - start_time
if __name__ == "__main__":
total_elapsed_time = [main() for i in range(10**4)]
print(f"elapsed time : {sum(total_elapsed_time)/len(total_elapsed_time)}")
이번에는 2개의 스레드를 사용하였으므로 단일 스레드보다는 빠르게 수행될 것으로 예상되지만 0.0004초가 걸렸습니다.
즉, CPU-bound 작업에 대해서는 멀티 스레드가 비효율적입니다.
import time
import threading
def work(start:int, end:int, total:list):
_ = 0
for i in range(start, end):
_ += i
total.append(_)
def main():
result = []
thread1 = threading.Thread(target=work, args=(0, 500, result))
thread2 = threading.Thread(target=work, args=(500, 1000, result))
start_time = time.time()
thread1.start()
thread2.start()
thread1.join()
thread2.join()
end_time = time.time()
return end_time - start_time
if __name__ == "__main__":
total_elapsed_time = [main() for i in range(10**4)]
print(f"elapsed time : {sum(total_elapsed_time)/len(total_elapsed_time)}")
💡 GIL에 의해 한 번에 하나의 스레드만 result에 접근이 가능하기 때문에 2개의 스레드가 생각보다 성능이 안 나옵니다. 또한 스레드 간 스위치가 이루어지는 시간까지 더해져 더욱 오래 걸린 것으로 추측됩니다.