Skip to content

Latest commit

 

History

History
198 lines (145 loc) · 7.29 KB

BetterWay71.md

File metadata and controls

198 lines (145 loc) · 7.29 KB

71. 생산자-소비자 큐로 deque 를 사용하라

1. 리스트를 사용한 생산자-소비자 큐

  • 생산자-소비자 큐

    • 선입선출 큐
    • FIFO 큐
  • 수신된 전자우편을 장기 보관하기 위해 처리하는 프로그램, 이 프로그램이 리스트를 생산자-소비자 큐를 사용한다고 가정

    • try_receive_email, 전자우편을 수신하는 역할을 하는 함수
      • 소켓, 파일 시스템 또는 다른 유형의 I/O 시스템을 사용해 전자우편 받을 수 있음
      • Email 인수턴스를 반환하거나 NoEmailError 예외 발생
    class Email:
        def __init__(self, sender, receiver, message):
            self.sender = sender
            self.receiver = receiver
            self.message = message
        ...
    class NoEmailError(Exception):
        pass
    
    def try_receive_email():
        # Email 인스턴스를 하나 반환하거나,# NoEmailError를 발생시킨다
        ...
    • 생산자 함수는 전자우편을 받아 나중에 소비할 수 있게 큐에 삽입
      • 리스트의 append 메소드를 사용해 새 메시지를 뒤에 추가
    def produce_emails(queue):
        while True:
            try:
                email = try_receive_email()
            except NoEmailError:
                return
            else:
                queue.append(email)  # 생산자 
    • 소비자 함수는 큐에 대해 pop(0) 호출, 리스트의 첫번째 원소를 제거하고 제거된 첫번째 값을 호출자에게 돌려줌
      • 소비자는 항상 큐 맨 앞에 있는 원소를 처리하여 원소 도착 순서대로 처리
    def consume_one_email(queue):
        if not queue:
            return
        email = queue.pop(0) # 소비자
        # 장기 보관을 위해 메시지를 인덱싱함
        ...
    • keep_running 함수가 False 를 반환할 때까지 생산과 소비를 반복하는 함수 loop 추가
    def loop(queue, keep_running):
        while keep_running():
            produce_emails(queue)
            consume_one_email(queue)
            
    def my_end_func():
        ...
    
    loop([], my_end_func)
    • 단점
      • 생산자-소비자 큐를 사용 여부는 지연 시간과 단위 사간당 throughput 사이의 trade-off

        • 생산자-소비자 큐를 사용할 때는 가능하면 빨리 원소를 수집하길 원하므로 새로운 원소를 받아들이는 지연 시간 최소화, 소비자 큐에 쌓인 원소를 일정한 throughput 으로 처리
        • 종단점 사이의 지연 시간을 희생함으로써 안정적인 성능 프로파일과 일관성 있는 throughput 달성 가능
      • 큐를 리스트로 사용할 때 원소 개수가 일정 개수를 초과하면 성능은 선형보다 더 나빠짐

        원소 : 500 걸린 시간: 0.000023
        
        원소 : 1,000 걸린 시간: 0.000045
        데이터 크기  2.0, 걸린 시간  2.0
        
        원소 : 2,000 걸린 시간: 0.000087
        데이터 크기  4.0, 걸린 시간  3.8
        
        원소 : 3,000 걸린 시간: 0.000134
        데이터 크기  6.0, 걸린 시간  5.8
        
        원소 : 4,000 걸린 시간: 0.000181
        데이터 크기  8.0, 걸린 시간  7.9
        
        원소 : 5,000 걸린 시간: 0.000231
        데이터 크기  10.0, 걸린 시간  10.1
        • 리스트 타입에 있는 append 메소드가 거의 상수 시간이 걸림
        • 데이터 크기가 커짐에 따라 큐에 데이터를 넣는 데 걸리는 전체 시간이 선형적으로 증가
      • 큐 맨 앞 원소를 제거하는 pop(0) 호출 성능

        원소 : 500 걸린 시간: 0.000043
        
        원소 : 1,000 걸린 시간: 0.000097
        데이터 크기  2.0, 걸린 시간  2.2
        
        원소 : 2,000 걸린 시간: 0.000252
        데이터 크기  4.0, 걸린 시간  5.8
        
        원소 : 3,000 걸린 시간: 0.000464
        데이터 크기  6.0, 걸린 시간 10.7
        
        원소 : 4,000 걸린 시간: 0.000751
        데이터 크기  8.0, 걸린 시간 17.3
        
        원소 : 5,000 걸린 시간: 0.001229
        데이터 크기  10.0, 걸린 시간  28.3
        • 큐 길이가 늘어남에 따라 큐 길이의 제곱에 비례해 증가
        • 리스트의 모든 남은 원소를 제 위치로 이동하는 작업 존재하기 때문
          • 대략 len(queue) * len(queue)

2. collections 내장 모듈의 deque

  • deque

    • 양방향 큐
    • 시작과 끝 지점에 원소를 삽입/제거하는데 상수 시간 소요
  • deque 를 사용한 예제

    • 생산자 함수의 append 는 그대로 사용
    • 소비자 함수에서는 popleft 메소드를 사용하도록 변경
    • loop 메소드 호출 시 deque 인스턴스 전달
import collections

def consume_one_email(queue):
    if not queue:
        return
    email = queue.popleft()  # 소비자
    # 전자우편 메시지를 처리한다
        ...
def my_end_func():
    ...

loop(collections.deque(), my_end_func)
  • 벤치마크 수행 결과

    # produce
    원소 : 500 걸린 시간: 0.000022
    
    원소 : 1,000 걸린 시간: 0.000044
    데이터 크기  2.0, 걸린 시간  2.0
    
    원소 : 2,000 걸린 시간: 0.000091
    데이터 크기  4.0, 걸린 시간  4.2
    
    원소 : 3,000 걸린 시간: 0.000142
    데이터 크기  6.0, 걸린 시간  6.5
    
    원소 : 4,000 걸린 시간: 0.000192
    데이터 크기  8.0, 걸린 시간  8.8
    
    원소 : 5,000 걸린 시간: 0.000244
    데이터 크기  10.0, 걸린 시간  11.1
    
    #consume
    원소 : 500 걸린 시간: 0.000019
    
    원소 : 1,000 걸린 시간: 0.000041
    데이터 크기  2.0, 걸린 시간  2.1
    
    원소 : 2,000 걸린 시간: 0.000081
    데이터 크기  4.0, 걸린 시간  4.2
    
    원소 : 3,000 걸린 시간: 0.000126
    데이터 크기  6.0, 걸린 시간  6.6
    
    원소 : 4,000 걸린 시간: 0.000169
    데이터 크기  8.0, 걸린 시간  8.8
    
    원소 : 5,000 걸린 시간: 0.000213
    데이터 크기  10.0, 걸린 시간  11.0
    • 생산자 함수에서의 성능은 리스트 사용에서의 성능과 비슷
    • 소비자 함수에서의 성능은 대기열 길이에 선형적으로 비례하여 증가

3. 정리

  • 프로그램에서 생산자-소비자 큐가 임계 단계라면 deque 로 변경하는 것이 성능상 좋음
  • 임계 단계임을 확신할 수 없는 경우 벤치마크를 사용하여 성능 측정을 통해 임계 단계인지 여부 확인 필요함