병렬처리

Jane의 study note.·2022년 10월 1일
0

파이썬 Python

목록 보기
27/31
post-thumbnail

병렬처리

여러개의 작업들이 동시에 일어나는 것
따라서 빨리 끝남

1. 프로세스와 스레드

프로그램(하드디스크) -> 윈도우 탐색기, 파일저정소

프로세스(메모리) -> ctrl+alt+del키 눌렀을 때 뜨는 작업관리자, 파일을 더블클릭해서 실행시켰을 때 프로세스가 뜬다(작동한다)고 하는 것, 이때 메모리가 할당됨

  • <정확한 정의>
  • 프로세스(작업장): 실행중인 프로그램, 자원과 쓰레드로 구성되어 있음
  • 쓰레드(일꾼): 프로세스 내에서 실제 작업을 수행 (실제 컴퓨터가 일을 하는 것)
    쓰레드가 여러개 있다는 건 일꾼이 여러 명 있다는 것
  • ※자원: 프로그램을 실행시켜 내용을 입력하면 작업관리자 메모리의 MB가 상승함, MB가 자원임
  • 파이썬은 인터프리터 언어로서 기본적으로 싱글 쓰레드에서 순차적으로 동작. 따라서 병렬처리를 위해선 별도의 모듈 을 사용하여 수행해야 한다. (threading모듈 제공함)
  • 구글코랩이나 주피터 노트북에서는 멀티 프로세싱이 잘 작동하지 않음 => 따라서 그냥 파이썬을 이용함
from threading import Thread   #멀티쓰레드=병렬처리

import time                     #작업 시간을 체크하기 위한 모듈

def work(work_id, start, end, result): #쓰레드를 하기 위해서는 기본적으로 함수를 정의하고 그 다음에 멀티쓰레드를 정의해야함

    total = 0
for i in range(start, end):
        total += i
result.append(total)


if __name__ == "__main__":  # 현재 실행하는 파일(메인파일,실행파일)일 때만 아래구문을 실행하고 다른 모듈을 불러오는 파일일 때는 실행하지 않는다.
    start = time.time()     # 코드가 실행되기 전 현재시간
    result = []
    th1 = Thread(target=work, args=(1, 0, 10000, result))   #target= 내가 적용시키고 싶은 함수, args=(매개변수에 대입되는 인자)
    th2 = Thread(target=work, args=(2, 10001, 20000, result)) 

    #th1 = Thread(target=work, args=(1, 0, 1000000, result))   
    #th2 = Thread(target=work, args=(2, 1000001, 2000000, result)) # 이때 print(time.time()-start)값은 0.15991878509521484

    #th1 = Thread(target=work, args=(1, 0, 2000000, result))  # 여긴 0.1575162410736084 멀티쓰레드와 거의 차이가 없음 ㅜㅋㅋ => 강의자료 p195, GIL 정책때문

    th1.start()                   # 위의th1은 단지 선언한 것이고 start()를 해야 비로소 함수가 실행됨
    th2.start()                   # 2가지가 병렬적으로 작업을 함

    th1.join()                    # join () 메소드는 파이썬에게 프로세스가 종료 될 때까지 대기하도록 지시합니다. 
                                  # 조인메서드가 없어도 작동하긴 하나 작업을 할때 중간에 프로세스가 종료되어 쓰레드가 일을 하다 말고 꺼져 문제가 생기는 경우를 방지하기 위함
                                  # 불안전한 종료를 방지하기 위한 함수
    th2.join()
print(sum(result))                # 199980000  : 0부터 10000-1까지 더한 값과 10001부터 20000-1까지 더한 값의 결과의 합
print(result)                     # [49995000, 149985000] : [첫번째 쓰레드가 작업한 값, 두번째 쓰레드가 작업한 값]

print(time.time()-start)          # 프린트가 실행되는 시점의 시간(time.time()) - 코드가 실행되기 전 시간(start)
                                  # 결국 코드가 실행된 충시간 표시됨
  • 아나콘다 파워쉘 프롬프트에서 cd 명령어 입력시 폴더가 한글명이면 오류뜸
  • 바탕화면 폴더 cd E:\jupyter notebook에서 test.py 작업,실행함
# 위의 코드 메모장에 입력 후 아나콘다 파워쉘 프롬프트에서 실행(python test.py 입력)한 결과값
# 199980000
# [49995000, 149985000]
# test2.py in E:\jupyter notebook
from multiprocessing import Pool 
import os
import time

def f(x):
    return x*x
    print(x, os.getpid()) ##os.getpid: pid는 프로세스 아이디
'''
print(os.getpid())
# 프로세스 아이디 13536, 9692, 8692 이 분배가 됨
13536
9692
8692
8692
9692
8692
13536
9692
'''

print(__name__)
'''
__main__
__mp_main__  #mp: 멀티 프로세스를 의미
__mp_main__  #Pool(4): 풀이 4개이므로 프로세스가 4개가 뜸, 8개로 고치면 8개의 프로세스가 뜸
__mp_main__  #이것의 의미는 
__mp_main__
'''
if __name__ == '__main__':   # 이 줄이 없으면 오류뜸: main프로세스에서 자식 프로세스 mp_main을 만들고 자식 프로세스가 또 자식 프로세스를 만듬, 무한반복해서 계속 내려감, 에러나는 이유
                             # if __name__ == '__main__'를 실행하면 자식프로세스는 윗줄은 실행되나 이줄 아래는 실행하지 않음
    start = time.time()                            
    p = Pool(4)              # 자식 프로세스를 4개를 만들겠다.
    result = p.map(f, range(8))  # map함수는 f함수에 collection(여기서는 range(8))을 p를 적당히 분배해서 result에 저장하라는 것
                                 #map함수(※이해인 참고)
                                 # 위의 함수의 return값이 여기 result로 들어감
    p.close()               # 자식 프로세스를 죽인다.
    print(result)           # [0, 1, 4, 9, 16, 25, 36, 49]
    print(sum(result))      # 140
    print(time.time()-start)   #range를 10000000(8개)으로 뒀을때 시간체크 (※#print(x, os.getpid()), #print(result)해야 끝없이 나오는 것 방지함)
                               #pool을 1개 쓸때(4.1068초), 2개 쓸때(2.7816초)와 4개 쓸때(2.36470초) 걸림
                               # 쓰레드를 2배 더 썼다고 시간이 절반으로 줄어드는 것음 아님
                               # pool은 자식 프로세스를 띄우는 건데 프로세스는 자원(메모리)과 쓰레드로 구성되어 있어 띄울때마다 메모리 복사도 되어야 한다. 
                               # 새롭게 프로세스가 떴으면 위의 f함수를 다시 불러와야 하고 range의 자원(메모리)도 다시 만들어줘야 한다.
                               # 프로세스를 띄우는 시간과 함수 작업하는 시간과의 차이가 있기 때문에 시간이 절반이 되지 않는다.

# 프로세스를 띄우는 시간보다 함수실행시간이 더 큰 경우
#ex> 함수의 return이    1) 함수실행시간을 크게: return [x*i for i in range(100000)]이면서 
#                       2) 프로세스 띄우는 시간을 작게: result = p.map(f, range(100)) 인 경우
#Pool(1)  1.67187초
#Pool(2)  1.05257초
#Pool(4)  0.95512초
#Pool(100) : 무리하게 프로세스를 늘려서 많이 띄우면 오류뜸 ValueError: need at most 63 handles, got a sequence of length 102
            #또한 CPU가 돌릴 수 있는 제한이 있기 때문에 프로세스를 많이 띄우면 얘가 효율적으로 자원분배를 못해 속도가 오히려 안나올 수 있다.
            #얼마를 늘릴지 그때그때 다르나 많이들 풀의 갯수를 코어의 개수만큼 쓴다.
# test3.py in E:\jupyter notebook
import os
from multiprocessing import Process # 위에서 수행한 Thread와 비슷함
def f(x):
    print(os.getpid())
    print(x*x)

print(__name__)
'''
__main__
__mp_main__
2392 #프로세스 아이디
1  #리스트0번째 연산값
__mp_main__
14380
4  #리스트1번째 연산값
__mp_main__
12044
16 #리스트4번째 연산값
__mp_main__
14200
9  #리스트3번째 연산값
'''

if __name__ == '__main__': 
    numbers = [1,2,3,4]  #풀은 이 숫자를 마음대로 풀어놓고 프로세스가 이 숫자를 알아서 넣게하는 것이고 프로세스는 정확하게 무엇을 풀지 선언을 해주는 것
                         #다만 병렬적으로 배정되는 것이기 때문에 순서대로 배정되지 않음
#proc1 = Process(target=f, args=(numbers[0],)) numbers가 지정되지 않았다는 에러뜸
    proc1 = Process(target=f, args=(numbers[0],)) #args=(numbers[0],: 콤마를 쓰는 이유는 튜플이기 때문에
    proc1.start()
    proc2 = Process(target=f, args=(numbers[1],))
    proc2.start()
    proc3 = Process(target=f, args=(numbers[2],))
    proc3.start()
    proc4 = Process(target=f, args=(numbers[3],))
    proc4.start()
    proc1.join()
    proc2.join()
    proc3.join()
    proc4.join()
  • Pool과 Process 모두 병렬 처리를 위해 사용되지만 차이가 존재한다. 쉽게 설명하자면, Pool은 처리할 일들 을 pool에 뿌려 놓고 알아서 병렬 처리를 하게 만드는 것이고 Process는 각 프로세스별로 할당량을 명시적으 로 적어 두고 그걸 처리하게 하는 것이다.
    물론 이는 이해를 돕기 위해 간단하게 설명했을 뿐 실제로 둘의 차이는 조금 더 복잡하다.

  • 정리: 쓰레드는 가볍지만 파이썬의 GIL 정책으로 인해 I/O 처리를 하는 경우에만 주로 효과적이고 프로세스는 각자 가 고유한 메모리 영역을 가지기 때문에 처음 프로세스를 만들 때 시간이 조금 필요하고 더 많은 메모리를 필 요로 하지만 병렬적으로 cpu 작업을 할 수 있어서 빠르다.

import time, os
from multiprocessing import Pool



def func(x):
    print("value {} is in Process ID : {}".format(x, os.getpid())) #os.getpid: pid는 프로세스 아이디
    return x**5

    

if __name__ == "__main__":
    start = int(time.time())
    num_pool = 100  # 2
    pool = Pool(num_pool)
    print(pool.map(func, range(100)))
    print("***run time(sec) :", int(time.time()) - start)
import time, os
from multiprocessing import Pool

def func(x):
    return x[0] * x[1]

    

if __name__ == "__main__":
    start = int(time.time())
    num_pool = 100  # 2
    pool = Pool(num_pool)
    print(pool.map(func, zip(range(100),range(100))))
    print("***run time(sec) :", int(time.time()) - start)
import time, os
from multiprocessing import Pool
from functools import partial

def func(x, y):
    return x * y

    

if __name__ == "__main__":
    start = int(time.time())
    num_pool = 100  # 2
    pool = Pool(num_pool)
    print(pool.map(partial(func,y= 3), range(100)))
    print("***run time(sec) :", int(time.time()) - start)

0개의 댓글