상세 컨텐츠

본문 제목

[인프런 리프 2기] 12. 파이썬 중급 과정 4주차(2) 병렬성

경험/2021 인프런리프2기

by mizu-umi 2021. 3. 31. 20:53

본문

728x90

관련글 ▼

[인프런 리프 2기] 10. 파이썬 중급 과정 3주차(3) 일급함수-1,2

[인프런 리프 2기] 10. 파이썬 중급 과정 3주차(4) 일급함수-3,4

[인프런 리프 2기] 11. 파이썬 중급 과정 4주차(1) 병행성

 


 

 

파이썬 병렬성 - Futures

  • 비동기 작업 처리
  • 파이썬 GIL 설명
  • 동시성 처리 실습 예제
  • Process, Thread 예제
  • Futures wait 예제
  • Futures as_completed
  • 동시 실행 결과 출력
  • 동시성 처리 응용 예제 설명

 

병렬성(Parallelism) 이란? 여러 컴퓨터가 여러 작업을 동시에 수행하며 취합은 한 곳에서 한다 -> 속도가 장점.

 

강의 듣기 전 Futures의 정의에 대한 내 예측: 이름이 futures인 만큼 단순한 동시성이 아니라 앞으로 벌어질 일에 대한 가정을 두고 코딩을 하는 걸 의미하는게 아닐까?

-> 네, 아니었습니다.



동기와 비동기

요약: 비동기 작업을 실행한다.

 

프로그래밍에서 동기(synchronous) 란?

동기는 말 그대로 동시에 일어난다는 뜻입니다.  요청과 그 결과가 동시에 일어난다는 약속인데요. # 바로 요청을 하면 시간이 얼마가 걸리던지 요청한 자리에서 결과가 주어져야 합니다.(출처: https://private.tistory.com/24 [공부해서 남 주자])
  • 예: A 가 작업을 하고 끝나면 B가 받아서 작업을 하고 EnD. 

 

비동기(asynchronous)란?

동기와는 반대로 요청과 결과가 동시에 일어나지 않는다는 뜻.
  • 예: A가 작업을 하는 동안 B도 작업을 하고 C도 작업을 하고 EnD

 

지연시간(Block) CPU 및 리소스의 낭비를 방지한다. 따라서 (File)Network I/O 관련 작업에서는 동시성 활용을 권장한다. 비동기 작업과 적합한 프로그램일 경우 압도적으로 성능이 향상된다.

 

왜? 한번에 여러 작업을 하니까.

 

GIL (Global Interpreter Lock)

파이썬에만 있는 특성으로 두 개 이상의 스레드가 동시에 실행될때 하나의 자원을 액세스하는 경우 문제점을 방지하기 위해 GIL이 실행된다. 즉, 리소스 전체에 lock이 걸린다. -> context switch(문맥 교환, 작업끼리 데이터를 이어받아 합침) 비용이 듬. 

 

따라서 스레드를 많이 쓴다고 좋은게 아니다.

 

GIL을 우회하는 법 : 꼭 필요한 작업은 멀티프로세싱을 사용, CPython을 사용. 프로그램에 적합한 로직을 보고서 멀티 프로세싱이 필요하면 프로세싱을 쓰고 단순한 사칙연산 정도면 스레드를 쓰는게 좋다.

 


Futures 모듈

비동기 실행을 위한 API를 고수준으로 작성해서 사용하기 쉽도록 개선한 모듈이다.

 

from concurrent.Futures

 

장점: 과거에는 따로따로 코딩했어야 했던 멀티스레딩/멀티프로세싱의 API가 통일 되어서 매우 사용하기 쉽다. 실행중인 작업 취소, 완료 여부 체크, 타임아웃 옵션, 콜백함수 추가, 동기화 코드 등을 매우 쉽게 작성이 가능하다. -> Promise 개념

 


 

concurrent.futures 사용법1 : map

import os
import time
from concurrent import futures

WORK_LIST = [10000, 100000, 1000000, 10000000]

WORK_LIST 안에 있는 숫자들까지의 range를 전부 합산해보자.

 

1. 우선 누적 합계 함수를 만든다.

def sum_generator(n):
    return sum(n for n in range(1, n+1))

 

2. main() 함수를 정의한다.

def main():
    # Worker Count
    worker = min(10, len(WORK_LIST))

    # 시작 시간
    start_tm = time.time()

    # 결과 건수
    with futures.ThreadPoolExecutor() as executor:
        result = executor.map(sum_generator, WORK_LIST)

    # 종료시간
    end_tm = time.time() - start_tm

    # 출력포맷
    msg = '\n Result -> {} Time : {:.2f}s'

    # 최종 결과 출력
    print(msg.format(list(result), end_tm))

 

3. 실행문을 작성한다.

# 실행 
if __name__ == '__main__':
    main()

찾아보니 보통 실행문은 이렇게 작성하는 것 같다.

 

하나하나 살펴보기

worker = min(10, len(WORK_LIST))
  • Worker Count : 강의하시는 선생님이 자주 쓰는 방법으로 코딩하신 거라는데 왜 저렇게 하셨는지는 설명 안해줌.

 

# 시작 시간
start_tm = time.time()

...

# 종료시간
end_tm = time.time() - start_tm
  • 시작 시간과 종료 시간 : 두 시간 관련 객체 사이에 있는 수행문이 실행된 시간과 종료된 시간을 구한다.

 

with futures.ThreadPoolExecutor() as executor:
	result = executor.map(sum_generator, WORK_LIST)
  • with...as 문 : 파일(혹은 수행문)을 열었다가 닫는 수행을 해준다. with 문 안에 들어가 있는 수행문을 시작했다가 끝내준다.

Thread vs Process

위에 작성한 프로그램을 스레드와 프로세스로 실행했을 경우의 CPU 이용율 비교.

 

P는 ProcessPoolExecutor이며 T는 ThreadPoolExecutor로, process 쪽이 이용율이 훨씬 올라간다. 왜? 직접적으로 CPU 프로세싱을 돌리니까.

 

 


 

concurrent.futures 사용법2 : wait, as_completed

import os
import time
from concurrent.futures import ThreadPoolExecutor, ProcessPoolExecutor, wait, as_completed

패키지나 모듈은 통째로 가져오기 보다 쓸것만 가져오는 게 좋다.

 

아까 작성한 WORK_LIST의 값을 더 크게 바꾸면 각 자료마다 호출한 작업에 걸리는 시간이 조금씩 다를 수 있고 모든 작업이 성공할 리도 없기 때문에 그런 것들을 await과 as_completed가 제어한다.

 

def sum_generator(n):
    return sum(n for n in range(1, n+1))

def main():
    # Worker Count
    worker = min(10, len(WORK_LIST))

    # 시작 시간
    start_tm = time.time()

여기까지는 1번 예제에서 작성한 것과 동일하다. 관건은 그 다음에 선언하는 변수이다.

 

def main():
    # Worker Count
    worker = min(10, len(WORK_LIST))

    # 시작 시간
    start_tm = time.time()

    # futures를 받는 변수 선언
    futures_list = []

    # 결과 건수
    with ProcessPoolExecutor() as executor:
        for work in WORK_LIST:
            # future만 반환할뿐
            future = executor.submit(sum_generator, work) 

            # 스케줄링
            futures_list.append(future)

            # 스케줄링 확인
            print('Scheduled for {} : {}'.format(work, future))
            print()

futures_list 라는 빈 리스트 객체를 만든 다음 for문을 이용해 그 안에 필요한 값들을 넣는다.

 

wait() 로 결과 출력하기

        # 결과
        result = wait(futures_list, timeout=7)

        # 성공
        print('Completed Task : ' + str(result.done))
        # done : 성공한 값.

        # 실패
        print('Pending ones after waiting for 7 secs : ' + str(result.not_done))
        # not_done : 실패값

        # 결과 값 출력
        print([future.result() for future in result.done])

위지는 for문 내부가 아닌 with문 내부로 result라는 값을 선언해준다.

 

timeout 옵션 : 할당한 시간 안에 마무리 짓지 못한 작업은 실패로 간주, 강제종료 시킨다.

 

as_completed로 결과 출력하기

        # 결과출력
        for future in as_completed(futures_list):
            result = future.result()
            done = future.done()
            cancelled = future.cancelled

            print('Future Result : {}, Done : {}'.format(result, done))
            print('Future Cancelled : {}'.format(cancelled))

wait()를 사용했을 때보다 훨씬 간략하게 작성할 수 있다.

 


< 강의 출처 - 인프런 우리를 위한 프로그래밍 파이썬 중급 (Inflearn Original) >

728x90
반응형

관련글 더보기

댓글 영역