파이썬 멀티프로세싱 프로세스
배경 지식
현재 시판중인 프로세서들은 모두 멀티코어-프로세서라는, 하나의 프로세서 안에 여러 개의 미니 프로세서1들이 들어가 있는 구조이다.
15여년 전 AMD 사가 프로세서 성능의 한계를 돌파하기 위해 두 개의 프로세서를 집어넣은 CPU를 개발한 것을 시작으로,
현재는 4개면 적은 거고, 많게는 12개까지 배치한 프로세서들이 생겨나고 있다.
이 프로세서는 일종의 소형화된 기존 프로세서이나,
최근에는 ARM/인텔 사의 빅-리틀 시스템이나 엔비디아 사의 쿠다-텐서 코어 같이 특정 작업에 전문화된 프로세서를 여러 종류 놓는 시스템으로 발전하는 추세이다.
이야기가 삼천뽀로 가는거같으니 결론만 말하자면, 파이썬에서는 멀티 코어 프로세서를 십분 활용할 수 있는 멀티프로세싱 기능을 제공한다.
프로세스와 멀티프로세싱이란
보통 코드를 짜고 실행하면 해당 코드는 하나의 프로세서만을 사용한다. 이 실행되는 코드는 프로세스라고 지칭하겠다.
정확히 말하자면, 프로세스는 실행되고 있는 프로그램을 일컫는 말이다. 즉 python xxx.py를 실행하면 하나의 프로세스가 생기는 것이다. 만약 실행 도중에 또 python xxx.py를 실행하면 프로세스가 하나 늘어나겠다.
1
2
3
4
5
6
7
8
9
def tosso():
print("tosso")
def tissi():
print("tissi")
if __name__ == "__main__":
tosso()
tissi()
이러한 코드가 있다 치자.
이 코드를 실행하면 tosso()와 tissi() 함수를 선언하고 실행하는 하나의 프로세스가 작동한다.
이 프로세스는 tosso()를 실행한 뒤 tissi()를 실행하는데, 인간의 욕심은 끝이 없다고 사람들은 tosso()와 tissi()를 동시에 실행시키고 싶게 되었다.2 어떻게 할까?
가장 쉬운 방법은 tosso()와 tissi()를 각각 실행하는 파이썬 파일(프로그램)을 두 개 만들어서 실행하는 것이다.
1
2
3
4
def tosso():
print("tosso")
if __name__ == "__main__":
tosso()
1
2
3
4
def tissi():
print("tissi")
if __name__ == "__main__":
tissi()
이렇게
그런데 인간의 욕심은 또 끝이 없기에 ‘굳이 두 개 만들지 말고, 하나의 프로그램 안에서 나눠버린 후 실행시킬 수 없을까’ 하는 생각을 가지게 되었다.
그리하여 등장한 것이 멀티 프로세싱이다.
멀티 프로세싱은 간단히 말해 하나의 프로그램을 여러 개의 프로세스로 구성하는 것이다.
위 사례를 들면, main함수 내에서 tosso()를 실행하는 프로세스 하나, tissi()를 실행하는 프로세스 하나를 만든 뒤 실행시킨다.
이러면 프로그램을 실행할 때, main함수를 돌리는 하나의 프로세스가 있고, 이 프로세스는 프로세스를 생성하여 실행시키는 일을 한다.
이때 main함수 프로세스를 부모 프로세스, 생성한 프로세스를 자식 프로세스라고 한다.
부모 프로세스는 자식 프로세스가 작동하는 동안 멈추고, 자식 프로세스의 일이 끝나면 다시 실행된다. 결국 전체 프로그램이 시작하고 끝날 때까지 프로세스 개수는 1(부모)->2(자식들)->1(부모) 개인 셈이다.
또한 CPU 코어 하나는 동시에 하나의 프로세스만 실행할 수 있다. 이 점에서 멀티프로세싱은 멀티 코어 프로세서 환경에서 대단한 이점으로 작용한다.
만약 코어 하나짜리 프로세서에 두 개 이상의 프로세스를 할당하면 코어 하나가 여러 프로세스를 돌아가면서 수행한다.3 프로세스를 실행하는 순서는 운영체제가 알아서 결정하는데, 결국 하나의 프로세스가 실행 중이면 나머지 프로세스들은 멈춰있어야 한다는 것이다.4
하지만 코어가 여러개이면 동시에 실행할 수 있는 프로세스 개수가 넓어지므로 멀티 코어 프로세서일수록 멀티프로세싱으로 프로세스를 여러 개 만들어 주는 것이 이득이라는 것이다.
파이썬은 multiprocessing이라는 라이브러리로 멀티프로세싱을 제공한다.
그러면 이를 어떻게 하냐? multiprocessing 라이브러리에서는 크게 두가지 방법을 제공하고 있다.
- Process
1
from multiprocessing import Process
로 불러온다.
- Pool
1
from multiprocessing import Pool
로 불러온다.
- Queue도 있지만, 본인은 다루지 않겠다.
Process
- Process는 함수를 실행하는 자식 프로세스 객체이다.
- 독립된 프로세스이기에 기본적으로 다른 프로세스 간 소통하지 않는다.
1 2 3
(변수명) = Process(target=(실행할 함수), args=(함수 인자)) (변수명).start() (변수명).join()
이렇게 실행한다.
- 인자를 놓는 args 값으로는 (인자1,인자2,…) 이렇게 괄호 안에 인자들을 놓는다.
- 주의할 점으론 인자가 하나일 경우에도 괄호를 씌워줘야 하고, 끝에 쉼표(,)를 붙여줘야 한다.
- 즉
args=(인자,)
이런식
- start()는 프로세스를 시작(즉 함수 실행)한다는 말이다.
- join()은 프로세스가 끝날 때까지 기다린다는 말이다. 다시 말해 자식 프로세스가 실행되는 동안 부모 프로세스의 작동을 일시중지한다.
- 만일 join()이 없으면 프로세스가 시작되어도 start() 다음 코드가 실행될 것이다.
- 따라서 여러 프로세스를 동시에 사용할 것이면 모든 프로세스를 먼저 start()해준 후 join()을 해야 한다.
간단한 예제
어딜가나 다 있는…1부터 100 출력을 예제로 들어보자.
0~99가 있는 배열을 만들어서 1씩 더한 후 출력하는 함수가 있다.
배열을 25개 단위로 4개씩 쪼개 각 프로세스에다 할당한 후 실행해보자.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
from multiprocessing import Process
from time import time
tosso = [i for i in range(100)]
def duhagi(tissi):
for i in tissi:
i+=1
print(i)
if __name__ == "__main__":
a = time()
p1 = Process(target=duhagi, args=(tosso[:25], ))
p2 = Process(target=duhagi, args=(tosso[25:50], ))
p3 = Process(target=duhagi, args=(tosso[50:75], ))
p4 = Process(target=duhagi, args=(tosso[75:100], ))
p1.start()
p2.start()
p3.start()
p4.start()
p1.join()
p2.join()
p3.join()
p4.join()
print("끝!")
실행 결과
결과를 잘 보면 1부터 100까지 순차 출력되는게 아닌 뒤엉키는 것을 볼 수 있는데,
이는 프로세스들의 실행 순서가 고정된 것이 아니기 때문이다.
즉 무조건 p1->p2->p3 순이 아닌 p3->p1->p2 이런 식으로 실행될 수 있다는 것이다.
위 코드를 계속 실행해 보면 출력 순서가 매번 바뀌는 것을 알 수 있을 것이다.
만일 위 코드에서 join()을 하지 않으면
이렇게 《끝!》이 맨 앞에 나와버린다.
위에서 말한 것처럼 프로세스가 끝날 때까지 기다리지 않고 start() 다음 코드, 즉 print(“끝!”) 코드를 실행해버리는 것이다.
join()은 이 외에도 프로세스가 끝나면 종료해주는 기능이 있으니
멀티프로세싱 작업 뒤에 또다른 작업이 있으면 join()을 해줘 자원 낭비를 줄이자.
실전 예제
다음은 본인이 모 프로젝트에서 사용한 멀티프로세싱 코드이다.
보안 유지를 위해 일부 변수명 및 값들을 익명 처리했음에 유의하라.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
def dataupload(models):
processes = []
for model in models:
url = f"CENSORED"
if model == 1:
my = a(model,url)
elif model == 2:
my = b(model,url)
elif model == 3:
my = c(model,url)
elif model == 4:
my = d(model,url)
elif model == 5:
my = e(model,url)
elif model == 6:
my = f(model,url)
elif model == 7:
my = g(model,url)
elif model == 8:
my = h(model,url)
elif model == 9:
my = i(model,url)
processes.append(Process(target=my.autoupload))
for process in processes:
process.start()
for process in processes:
process.join()
해당 코드는 model 정보들이 담겨있는 models 배열을 인자로 받아 model 정보에 따라 model,url 속성을 갖는 클래스(a,b,c,…)를 생성하여 클래스의 autoupload 메소드를 실행하는 멀티프로세스를 만들어 실행하는 함수이다.
즉 models가 [1,2,3,4]이면 a,b,c,d 클래스의 autoupload를 각각 수행하는 프로세스 4개가 동시 실행된다는 것이다.
쓰레딩 문서에서 다룰 예정이지만, multiprocessing은 〈여러 콘솔에서 파이썬 함수를 각각 실행하는 것에 가깝기 때문에〉 경쟁 조건이 일어나지 않는다.
설사 프로세스들이 같은 변수를 인자로 갖는다 해도, 실제 함수에서 쓰는 변수는 해당 변수의 복제품이기 때문에 변수를 공유하는 것이 아니다.
이를 정확하게 알기 위해서는 주소값, 얉은 복사, 깊은 복사,… 등등을 알아야 하기 때문에… 이것도 차근차근 다뤄보겠다.
마치며
이상이다.
- 작성 예정 글
- 멀티프로세싱 풀
- 쓰레딩