파이썬으로 멀티 스레드(Multi-Thread)를 구현하는 방법에 대해서 살펴보고자 한다.
1. 싱글 스레드
import time
def do_something():
print('Sleeping 1 seconds')
time.sleep(1)
print('Done Sleeping...')
if __name__ == '__main__':
start = time.perf_counter()
for _ in range(10):
do_something()
finish = time.perf_counter()
print(f'Finished in {round(finish-start, 2)} second(s)')
위 코드는 do_something 함수를 10번 실행하는 코드이고 time.perf_counter() 함수를 통해서 소요 시간을 측정한다.
[실행결과]
실행 결과 단 하나의 스레드를 사용했을 때 10.1초 정도 소요되는 것을 확인할 수 있다.
2. threading 모듈을 이용한 멀티 스레드
import time
import threading # threading 모듈 import
def do_something():
print('Sleeping 1 seconds')
time.sleep(1)
print('Done Sleeping...')
if __name__ == '__main__':
start = time.perf_counter()
# 스레드를 담을 리스트 threads 초기화
threads = []
for _ in range(10):
# 스레드는 threading.Thread를 이용하여 생성,
# 스레드가 실행할 함수를 target 인자에 넣어줌
t = threading.Thread(target=do_something)
t.start()
threads.append(t)
# 스레드를 실행한 후 모든 스레드가 실행이 완료된 후
# 다음 코드를 실행시키기 위하여 join을 이용
for thread in threads:
thread.join()
finish = time.perf_counter()
print(f'Finished in {round(finish-start, 2)} second(s)')
위 코드는 do_something 함수를 각 10개의 스레드(Multi-Thread)를 만들어서 실행시키는 코드이다.
[실행결과]
실행 결과 10개의 스레드를 사용했을 때 1.2초 정도 소요되는 것을 확인할 수 있다.
3. concurrent 모듈 이용한 멀티 스레드
threading 모듈을 이용하여 멀티 스레드를 구현하면 스레드마다 start와 join을 해줘야하는 불편함이 있다. 하지만 concurrent를 사용한다면 위와 같은 귀찮은 작업을 안해도 된다.
import time
from concurrent import futures
def do_something():
print('Sleeping 1 seconds')
time.sleep(1)
return 'Done Sleeping...'
if __name__ == '__main__':
start = time.perf_counter()
# with 문을 사용하여 futures.ThreadPoolExecutor 클래스의 인스턴스(executor)를 만듦
# submit을 이용하여 스레드 Pool 에 작업(함수)을 제출
with futures.ThreadPoolExecutor() as executor:
results = [executor.submit(do_something) for _ in range(10)]
# result 메서드를 이욯하여 결과 반환
for f in futures.as_completed(results):
print(f.result())
finish = time.perf_counter()
print(f'Finished in {round(finish - start, 2)} second(s)')
[실행결과]
결과는 threading 모듈을 이용한 결과와 동일하다. 따라서 threading 모듈 뿐만 아니라 concurrent 모듈을 사용하는 것도 고려해보자.
4. 멀티 스레드는 언제 사용해야 할까?
1부터 100,000,000까지 합을 구하는 것을 생각해보자.
먼저 싱글 스레드로 구현해보자.
import time
from concurrent import futures
def cal_sum(input_list):
res = 0
for i in range(input_list[0], input_list[1]+1):
res += i
return res
if __name__ == '__main__':
start = time.perf_counter()
results = cal_sum([1,100000000])
print(results)
finish = time.perf_counter()
print(f'Finished in {round(finish-start, 2)} second(s)')
[실행결과]
싱글 스레드 결과로 3.05초가 걸린다.
이번엔 멀티 스레드(2개)를 이용해보자.
import time
from concurrent import futures
def cal_sum(input_list):
res = 0
for i in range(input_list[0], input_list[1]+1):
res += i
return res
if __name__ == '__main__':
start = time.perf_counter()
with futures.ThreadPoolExecutor() as executor:
sub_routine = [[1,100000000//2], [100000000//2+1,100000000]]
results = executor.map(cal_sum, sub_routine)
print(sum(results))
finish = time.perf_counter()
print(f'Finished in {round(finish-start, 2)} second(s)')
실행 결과 시간이 줄어들지 않았다. 이전에 10개 스레드를 사용했을 때 소요 시간이 1/10배로 감소하는 것을 확인하였기에, 이번에도 2개의 스레드를 사용한다며 1/2배로 감소할 것이라 예측했지만 실행 결과 큰 차이를 보이지 않았다. 그 이유는 파이썬 정책인 GIL(Global Interpreter Lock)으로 인하여 스레드 하나에 CPU 자원을 다 쓰기 때문이다. 따라서 단위를 2개로 쪼개도 하나의 스레드가 합을 처리하는데 CPU 자원을 다 써서 이 쓰레드가 끝나기 전에는 다른 스레드가 일을 할 수 없어서 시간 절감 표과가 크지 않은 것이다.
그렇다면 do_something 예제에서는 시간이 왜 1/10배로 감소할 수 있었던 것일까? 이는 do_something 함수가 I/O(입출력) 작업이기 때문이다. 이는 I/O 작업은 CPU 작업이 아니라서 GIL의 영향을 받지 않는다. 따라서 멀티 스레드의 효과를 톡톡히 볼 수 있었다.
따라서 반복 작업이 I/O작업으로 이루어져있다면 멀티스레드를 이용하여 성능을 개선할 수 있다.
'Python' 카테고리의 다른 글
멀티 프로세스(Multi-Process) 응용하기 with Python (0) | 2022.05.19 |
---|---|
멀티 스레드(Multi-Thread ) 응용하기 with Python (0) | 2022.05.19 |
멀티 프로세스 vs 멀티 스레드 (0) | 2022.05.18 |
[Python] 타입 어노테이션/힌트 (0) | 2022.04.11 |
[Python] 캡슐화 (0) | 2022.04.08 |
댓글