이 포스팅에서는 파이썬 (Python)의 멀티스레딩 (Multithreading)과 멀티프로세싱 (Multiprocessing)의 일을 할 수 있는 기능에 관하여 알아 보겠습니다. 이들은 하나나 여러 프로세스 내에서 여러가지의 오퍼레이션을 동시에 수행 할 수 있게 하여 줍니다. 동시에 일을 병렬적으로 처리하는 것은 시스템의 스피드와 효율성을 높여주죠.
기본 지식을 습득한 후에 예제를 리뷰 해 보겠습니다. 먼저 병렬 시스템 (parallel system)의 이로운 점은 아래와 같습니다.
향상된 성능: 동시에 여러가지의 일을 할 수 있으므로 처리의 속도가 빨라지고 시스템의 성능이 향상됩니다
확장성: 하나의 큰 일을 여러가지의 서브 태스크로 나누고 코어나 스레드를 연결하여 독립적인 처리를 가능하게 하여 대규모의 시스템에도 적합합니다.
효율적인 I/O 오퍼레이션: 동시 처리를 하면서 CPU는 프로세스가 끝나기를 기다릴 필요가 없게 되고 이전의 프로세스가 I/O를 차지하기 전까지는 바로 다음 프로세스를 처리할수 있습니다.
리소스의 최적화: 리소스를 나누면서 하나의 프로세스가 리소스의 전체를 독차지하는 것을 막을 수 있습니다.
다음은 멀티스레딩과 멀티프로세싱의 다른점에 관하여 알아 보겠습니다.
멀티스레딩 (Multithreading)이란?
멀티스레딩은 하나의 프로세스가 병행성을 가지면서 동시에 여러가지의 태스크를 할 수 있게 하여 주는 방법입니다. 하나의 프로세스내에서 여러개의 스레드가 만들어 질 수 있고 여러가지의 작은 태스크들이 병행으로 처리가 됩니다.
하나의 프로세스내의 스레드들은 공통 메모리 공간을 공유하게 됩니다, 그러나 그들의 스택 추적 (stack traces)이나 레지스터 (registers)는 분리되어 있습니다. 공유된 메모리로 인해 계산 값이 비싸지 않은 것이 장점입니다.
멀티스레딩은 대개는 I/O 오퍼레이션에 사용이 됩니다. 예를 들면 프로그램의 어떤부분이 I/O 오퍼레이션으로 바빠도 남은 부분들이 반응을 하게 하죠. 그렇지만 파이썬에서는 멀티스레딩으로 완전한 병행성을 가질 수 없습니다. Global Interpreter Lock (GIL) 때문이죠.
Global Interpreter Lock (GIL) 자세히 톺아보기 https://2022.pycon.kr/program/talks/19
간단히 요약하면, GIL은 단 하나의 스레드만 파이썬의 바이트코드 (bytecode) 상호작용을 하도록 하는 mutex lock입니다. 예를 들면 멀티스레드 모드에서도 한번에 하나의 스레드만 바이트코드를 실행하게 합니다.
이유는 CPython의 스레드 보호차원이지만, 이 이유로 멀티스레딩의 성능을 제한하고 있죠. 이 문제를 해결하기 위해, 파이썬의 멀티스레딩 라이브러리가 있습니다. 이 부분은 차차 설명 하겠습니다.
대몬 스레드 (Daemon Threads)란?
대몬 스레드는 백그라운드에서 계속 실행이 되는 스레드입니다. 이들의 주된 임무는 메인 스레드나 대몬 스레드가 아닌 스레드를 서포트하는 역할을 합니다. 대몬 스레드는 구동이 될때 메인 스레드를 막지 않고 완료가 되어도 계속 구동이 되는 스레드입니다.
파이썬에서는 대몬 스레드는 대개 garbage collector용도로 사용이 됩니다. 필요 없는 객체를 메모리에서 없애면서 메인 스레드가 구동이 잘 되게 리소스의 사용을 최적화 시켜주죠.
멀티프로세싱 (Multiprocessing)이란?
멀티프로세싱은 여러개의 프로세스를 병렬적으로 처리하는 데에 사용이 됩니다. 여러 프로세스를 각각의 메모리 장소에서 실행을 하여 병렬성을 달성하게 됩니다. CPU 내의 각각의 코어를 사용하여 프로세스 간의 소통에 도움을 주죠.
공유된 메모리 공간을 사용하지 않으므로 멀티프로세싱의 계산 가격은 멀티스레딩보다 더 높습니다. 그래도 독립적인 실행을 하면서 GIL의 제한에서 벗어날 수 있는 장점이 있습니다.
위의 이미지는 하나의 메인 프로세스가 두개의 프로세스를 만들어 각각에게 일을 맡기는 멀티프로세싱 환경을 보여주고 있습니다.
멀티스레딩 (Multithreading)의 예제
파이썬에는 멀티스레딩을 만들어 주는 빌트인 모듈이 있습니다
사용된 라이브러리:
제곱을 계산하는 함수:
숫자의 제곱을 계산하는 간단한 함수예제 입니다. 수의 리스트가 인풋으로 제공되고 제곱 계산이 완료된 수들과 사용된 스레드 및 프로세스 ID가 아웃풋으로 보내어 집니다.
메인 함수:
숫자의 리스트가 있고 first_half와 second_half로 나눕니다. 그리고 나누어진 리스트에 t1과 t2의 스레드를 지정합니다.
Thread 함수는 숫자 argument를 이용해 실행하는 함수를 새로운 thread로 만들게 됩니다. thread의 이름은 마음대로 지정이 가능합니다.
.start() 함수는 thread을 실행하고 .join() 함수는 실행된 thread가 일을 마치기 전까지 메인 thread를 막게 됩니다.
결과:
주의: 위의 모든 thread는 daemon thread가 아닙니다. daemon thread를 만들려면 t1.setDaemon(True)의 코드를 사용하면 t1이라는 이름의 thread는 daemon thread가 됩니다.
결과를 보면 프로세스 ID가 두개의 다른 thread에 동일하게 보입니다. 이는 두개의 thread는 동일한 프로세스이다 라는 뜻입니다.
그리고 결과가 순차적으로 보이지 않죠. 첫번째 라인은 thread1에서 나온 결과이고, 세번째 라인을 보면 thread2에서 보여진 결과, 그후에 다시 thread1에서 결과가 네번째 라인에 보입니다. 이것을 보면 일처리가 동시에 실행이 되었다는 것을 알게 됩니다.
동시에 처리가 되어도 병행적으로 처리가 되었다는 말은 아닙니다 왜냐하면 한번에 하나의 thread만 실행이 되어서 입니다. 그래서 실행 속도가 빨라지는 것은 아닙니다. 시퀀셜 실행과도 같은 시간이 걸리게 됩니다. CPU가 thread를 실행해도 중간에 다른 thread를 처리하러 thread 사이를 왔다갔다 하게 되는 것이죠.
멀티프로세싱 (Multiprocessing)의 예제
위에서 본것과 같이 이제는 멀티스레딩의 한계를 아실 수 있죠. 이번에는 멀티프로세싱을 이용하여 이 한계점을 넘어 보겠습니다.
위와 같은 예제를 사용하지만 두개의 thread보다 두개의 독립된 프로세스를 만들겠습니다.
사용된 라이브러리:
멀티프로세싱 모듈을 사용하여 독립된 프로세스를 만듭니다.
제곱을 계산하는 함수:
함수는 같지만 thread 정보를 알려주는 프린트 statement가 없어졌습니다.
메인 함수:
수정된 코드가 몇몇 있습니다. thread 대신에 프로세스를 만들었습니다.
결과:
결과를 보면 프로세스 ID가 다른 것을 보게 됩니다. 이는 독립된 프로세스가 실행이 되었다는 의미입니다. 프로세스가 병행되어 실행이 되었는지는 아래의 두개의 다른 환경의 결과 값으로 알아 보겠습니다.
런타임 (runtime) 실행 시간 비교 : 멀티프로세싱과 그렇지 않은 경우
병행적으로 처리가 되었는지는 두 가지 환경 (멀티프로세싱과 그렇지 않은 경우)의 런타임 실행시간을 비교하면 됩니다.
이 예제를 위해서 10^6 개의 integer를 이용하여 보겠습니다. random 라이브러리를 이용하여 숫자 리스트를 만들고 time 모듈을 이용하여 런타임 실행시간을 기록합니다.
결과:
결과를 보면 멀티프로세싱이 두배나 빠르다는 것을 알수 있습니다. 이 결과로 두개의 프로세스가 동시에 병행으로 실행이 되었다는 것을 알 수 있습니다.
참고: