개인 공부 후 자료를 남기기 위한 목적이므로 내용 상에 오류가 있을 수 있습니다.
2월 8일(화)
Python multi thread & multi process
파이썬은 인터프리터 언어로서 기본적으로 싱글 쓰레드(thread)에서 순차적으로 동작한다.
따라서 병렬처리를 하기 위해서는 별도의 모듈을 사용하여 구현해야 한다.
threading 모듈로 multi thread 구현
아래의 예제는 0부터 100,000,000 까지의 합을 구하는 계산 프로그램을 하나의 쓰레드로 동작하게 하는 코드이다.
from threading import Thread
def work(id, start, end, result):
total = 0
for i in range(start, end):
total += i
result.append(total)
return
if __name__ == "__main__":
START, END = 0, 100000000
result = list()
th1 = Thread(target=work, args=(1, START, END, result))
th1.start()
th1.join()
print(f"Result: {sum(result)}")
쓰레드는 threading 모듈의 Thread() 함수로 쓰레드 객체를 받아서 사용한다. Thread() 함수의 target 파라미터는 쓰레드가 실행할 함수, args 파라미터는 그 함수의 인자들을 의미한다. 또한, start() 함수로 쓰레드를 시작하고 join() 함수로 쓰레드가 끝날 때까지 기다린다.
위의 예제코드의 실행시간은 약 5초가 걸린다.
다음의 예제는 위의 예제코드에 쓰레드를 추가하여 병렬로 동작하는 코드를 만든 것이다.
from threading import Thread
def work(id, start, end, result):
total = 0
for i in range(start, end):
total += i
result.append(total)
return
if __name__ == "__main__":
START, END = 0, 100000000
result = list()
th1 = Thread(target=work, args=(1, START, END//2, result))
th2 = Thread(target=work, args=(2, END//2, END, result))
th1.start()
th2.start()
th1.join()
th2.join()
print(f"Result: {sum(result)}")
th2 라는 쓰레드 객체를 추가했고, 쓰레드에서 실행되는 함수에 들어가는 인자를 절반씩 나누어 입력하여 따로 계산하도록 했다. 이 코드를 실행하면 하나의 프로세스에서 동작하지만, 여러 cpu를 가지고 있다면 쓰레드가 적절히 분산되어 병렬 처리를 할 것으로 예상된다.
하지만 위의 예제코드의 실행시간도 대략 5초가 걸렸다.
위의 예제코드는 하나의 쓰레드로 동작시킨 것과 별반 다르지 않다.
이는 파이썬의 GIL 정책 때문이다.
GIL(Global Interpreter Lock)
언어에서 자원을 보호하기 위해 락(Lock) 정책을 사용하고 그 방법 또한 다양하다. 파이썬에서는 하나의 프로세스 안에 모든 자원의 락(Lock)을 글로벌(Global)하게 관리함으로써 한 번에 쓰레드만 자원을 컨트롤하여 동작하도록 한다.
위의 코드에서 result라는 자원을 공유하는 두 개의 쓰레드를 동시에 실행시키지만, 결국 GIL 때문에 한 번에 하나의 쓰레드만 계산을 실행하여 실행 시간이 비슷한 것이다. 즉, GIL 때문에 자원관리를 더욱 쉽게 할 수 있지만 지금처럼 멀티코어가 당연한 시대에서는 조금 아쉬운 것이 사실이다. 그렇다고 파이썬의 GIL이 쓸모 없다는 것은 아니다. 사실상 GIL이 적용되는 것은 cpu 동작에서이고 쓰레드가 cpu 동작을 마치고 I/O 작업을 실행하는 동안에는 다른 쓰레드가 cpu 동작을 동시에 실행할 수 있다. 따라서 cpu 동작이 많지 않고 I/O 동작이 더 많은 프로그램에서는 멀티 쓰레드만으로도 성능개선의 효과를 얻을 수 있다.
multiprocessing 모듈로 멀티 프로세스 구현
이러한 상황에서 계산을 병렬로 처리하는데 도움을 주는 것이 바로 multiprocessing 모듈이다. multiprocessing 모듈은 쓰레드 대신 프로세스를 만들어 병렬로 동작한다.
다음의 예제는 위의 계산 프로그램을 멀티 프로세스로 구현한 코드이다.
from multiprocessing import Process, Queue
def work(id, start, end, result):
total = 0
for i in range(start, end):
total += i
result.put(total)
return
if __name__ == "__main__":
START, END = 0, 100000000
result = Queue()
th1 = Process(target=work, args=(1, START, END//2, result))
th2 = Process(target=work, args=(2, END//2, END, result))
th1.start()
th2.start()
th1.join()
th2.join()
result.put('STOP')
total = 0
while True:
tmp = result.get()
if tmp == 'STOP':
break
else:
total += tmp
print(f"Result: {total}")
multiprocessing 모듈의 가장 큰 장점은 threading 모듈과 구현 방식이 거의 같아서 기존의 쓰레드 방식으로 구현한 코드를 쉽게 대체할 수 있다는 것이다. 위의 코드에서 변경된 것은 Thread 함수가 아닌 Process 함수에서 객체를 받아 사용하는 것과 result로 Queue 객체를 사용한 것 뿐이다. 해당 코드를 실행하면 실행시간이 약 2.5초로 위의 쓰레드 프로그램의 절반 수준으로 감소하는 것을 확인할 수 있다.
프로세스는 각자가 고유한 메모리 영역을 가지기 때문에 쓰레드에 비하면 메모리 사용이 늘어난다는 단점이 있지만, 이 방식을 통해 싱글머신 아키텍처에서 여러 머신을 사용하는 분산 프로그램으로 쉽게 전환할 수 있다. 참고로, 각각의 프로세스가 자신만의 메모리 공간을 사용해야 하기 때문에 프로세스간 데이터 교환을 위해서는 multiprocessing.Queue 객체를 사용해야 한다.
결론
결론적으로 말하자면, 파이썬에서 병렬처리를 구현하는 방식은 멀티 쓰레드를 사용하는 방법과 멀티 프로세스를 사용하는 두 가지 방법이 있다. 쓰레드는 가볍지만 GIL로 인해 계산처리를 하는 작업은 한 번에 하나의 쓰레드에서만 작동하여 cpu 작업이 적고 I/O 작업이 많은 병렬처리 프로그램에서 효과를 볼 수 있다. 반면에 프로세스는 각자의 고유한 메모리 영역을 가지기 때문에 더 많은 메모리를 필요로 하지만, 각 프로세스에서 병렬로 cpu 작업을 할 수 있고 이를 활용하여 여러 머신에서 동작하는 분산처리 프로그래밍도 구현할 수 있다.
참고출처
'기타' 카테고리의 다른 글
TIL - 22.02.10 (0) | 2022.02.10 |
---|---|
TIL - 22.02.09 (0) | 2022.02.09 |
TIL - 22.02.07 (0) | 2022.02.07 |
TIL - 22.02.04 (0) | 2022.02.04 |
TIL - 22.02.03 (0) | 2022.02.03 |
댓글