2009-05-12 2 views
6

나는 최근에 간단한 생산자/소비자 패턴을 사용하는 프로그램을 작성했습니다. 처음에는 스레딩의 부적절한 사용과 관련된 버그가있었습니다. 그러나 그것은 잠김없는 방식으로 생산자/소비자 패턴을 구현할 수 있는지 생각하게했습니다.파이썬 생산자 - 소비자 잠금없는 접근 방식이 스레드로부터 안전한가요?

  • 한 프로듀서 스레드 : 내 경우에는

    요구 사항은 간단했다.

  • 소비자 스레드 한 개.
  • 대기열에는 하나의 항목에 대한 장소가 있습니다.
  • 생산자는 현재 항목이 소비되기 전에 다음 항목을 생성 할 수 있습니다. 따라서 현재 아이템은 없어지지 만 괜찮습니다.
  • 소비자는 다음 항목이 생성되기 전에 현재 항목을 소비 할 수 있습니다. 따라서 현재 항목은 두 번 (또는 그 이상) 소비되지만, 괜찮습니다.

그래서 내가 쓴 :

QUEUE_ITEM = None 

# this is executed in one threading.Thread object 
def producer(): 
    global QUEUE_ITEM 
    while True: 
     i = produce_item() 
     QUEUE_ITEM = i 

# this is executed in another threading.Thread object 
def consumer(): 
    global QUEUE_ITEM 
    while True: 
     i = QUEUE_ITEM 
     consume_item(i) 

내 질문은 :이 코드 스레드 - 안전합니까?

즉석 코멘트 :이 코드는 실제로는 잠금이 없습니다. CPython을 사용하며 GIL이 있습니다.

코드를 조금 테스트했는데 제대로 작동하는 것 같습니다. 그것은 GIL 때문에 원자 적 인 일부로드 및 저장 연산으로 변환됩니다. 그러나 x가 __del__ 메소드를 구현할 때 del x 조작이 원자가 아님을 또한 알고 있습니다. 따라서 내 항목에 __del__ 메쏘드가 있고 약간의 불규칙한 스케쥴링이 발생하면 일이 깨질 수 있습니다. 안 그래요?

또 다른 질문은 위의 코드가 제대로 작동하도록하려면 어떤 종류의 제한 사항 (예 : 생산 된 항목 유형)을 사용해야합니까?

내 질문은 이론적으로 CPython 및 GIL의 단점을 악용하여 잠금이 필요 없음 (즉, 코드에서 threading.Lock을 명시 적으로 잠그지 않음) 솔루션을 제시합니다.

+0

왜 __del__ 메서드를 작성 하시겠습니까? –

답변

2

예 이것은 당신이 설명하는 방식으로 작동합니다 : 생산자는 건너 뛸 요소를 생성 할 수

  1. 있다.
  2. 소비자가 동일한 요소를 소비 할 수 있음을 나타냅니다.

은 그러나 나는 또한 델 X 작업이 때 X를 구현 방법 원자되지 않았 음을 알 수 있습니다. 따라서 내 항목이 메쏘드이고 약간의 불규칙한 스케쥴링이 발생하면 일이 깨질 수 있습니다.

여기에 "del"이 표시되지 않습니다. 만약 del가 consume_item에서 발생하면 del이 생성자 스레드에서 발생할 수 있습니다. 나는 이것이 "문제"라고 생각하지 않는다.

그래도 사용하지 마십시오. 무의미한 폴링 사이클에서 CPU를 사용하게 될 것이고, 파이썬에는 이미 전역 잠금이 있기 때문에 잠금이있는 대기열을 사용하는 것보다 빠르지 않습니다.

+0

'__del__ '에 의해 객체의 참조 횟수가 0으로 떨어질 수 있으므로'__del__' 메소드가 호출됩니다. 이것은 몇 가지 문제를 일으킬 수 있지만, 괜찮다고 말하면 CPython에있는 것들이 그렇게되기를 바랍니다. – Jasiu

1

소비자가 그것을 소비하기 전에 생산자가 QUEUE_ITEM을 덮어 쓸 수와 소비자 두 번 QUEUE_ITEM를 소비 할 수 있기 때문하지 정말 스레드 안전합니다. 당신이 언급했듯이, 당신은 그것으로 괜찮습니다. 그러나 대부분의 사람들은 그렇지 않습니다.

cpython 내부에 대한 지식이있는 사람은 이론적 인 질문에 답을해야합니다.

+0

예, 내 코드가 threadsafe도 lockless도 아닙니다. :) 여기서 'threadsafe'란 뜻은 : 충돌하지 않고, 메모리를 손상시키지 않으며, 교착 상태에서 멈추지 않고 내 요구 사항에 설명 된대로 작동합니다. – Jasiu

+0

나는 GIL이 방금 언급 한 유형의 오류로부터 당신을 보호 할 것이라고 믿습니다. GIL은 파이썬의 내부 상태를 스레드의면에서 올바르게 유지합니다. 코드가 예상대로 작동하지 않을 수도 있지만 (근본적으로 이미 경쟁 조건은 원하는대로 괜찮다고 말했지만), 통역사의 내부 상태가 통역사의 관점에서 안전하지 않을 것이라고 생각하지 않습니다. 길에 의해 지키게된다. – Doug

0

특히 항목이 큰 개체 인 경우 스레드가 생성/소비되는 동안 중단 될 수 있습니다. 편집 : 이것은 단순한 추측입니다. 나는 전문가가 아니다.

또한 스레드는 다른 하나가 실행되기 전에 여러 항목을 생성/소비 할 수 있습니다.

+0

그게 좋은 지적이야. 내가 생각하지 못한 가능성을 가져온다. AFAIK, 파이썬은 신호 마스크를 사용하여 각 연산 코드를 실행하므로 인터럽트되지 않고 원자 적입니다. 그렇지 않으면 일들이 불쾌해질 것이라고 생각합니다. 정규 파이썬의 경우에도 멀티 스레드로 작동하지 않습니다. – Jasiu

0

두 항목 모두 원 자성이므로/pop을 추가하는 한 목록을 대기열로 사용할 수 있습니다.

QUEUE = [] 

# this is executed in one threading.Thread object 
def producer(): 
    global QUEUE 
    while True: 
     i = produce_item() 
     QUEUE.append(i) 

# this is executed in another threading.Thread object 
def consumer(): 
    global QUEUE 
    while True: 
     try: 
      i = QUEUE.pop(0) 
     except IndexError: 
      # queue is empty 
      continue 

     consume_item(i) 

아래의 클래스 범위에서 대기열을 지울 수도 있습니다.

class Atomic(object): 
    def __init__(self): 
     self.queue = [] 

    # this is executed in one threading.Thread object 
    def producer(self): 
     while True: 
      i = produce_item() 
      self.queue.append(i) 

    # this is executed in another threading.Thread object 
    def consumer(self): 
     while True: 
      try: 
       i = self.queue.pop(0) 
      except IndexError: 
       # queue is empty 
       continue 

      consume_item(i) 

    # There's the possibility producer is still working on it's current item. 
    def clear_queue(self): 
     self.queue = [] 

생성 된 바이트 코드를보고 원자가되는 목록 연산을 찾아야합니다.

+0

당신이 방금 전역 변수 읽기/쓰기에서 내 질문을 이동하여/목록에 추가 할 수 있지만 질문은 남아 있습니다 : __del__을 호출하는 일부 불쾌한 일정이 발생하더라도 내 코드가 작동합니까? – Jasiu

+0

__del__을 명시 적으로 호출하거나 del을 호출하여 호출 하시겠습니까? del은 즉시 삭제하지 않습니다. 참조 횟수가 줄어 듭니다. 소비자가 참고 자료를 보유하고있는 한 괜찮습니다. – null

+0

다음 시나리오를 고려하십시오. 1. 대기열에 많은 항목이 포함되어 있습니다. 2. 소비자 호출은 clear_queue입니다. 3. 대기열에있는 항목의 참조가 0으로 떨어집니다. 4. __del__ 메서드가 호출됩니다. 5.이 모든 것은 "self.queue = []"문에서 발생합니다. 6. 한편 제작자는 다른 항목을 추가하려고합니다. "self.queue = []"를 "del self.queue [:]"로 바꿀 수는 있지만 문제는 "self.queue"속성에 액세스하는 것이 파이썬의 내부 목록 작업으로 옮겨가는 것입니다. 그래서 IMHO는 문제를 전역 변수 읽기/쓰기에서 파이썬의 내부 목록 내부 읽기/쓰기로 옮깁니다. – Jasiu

6

위조가 당신을 물으십시오. 스레드간에 통신하려면 Queue를 사용하기 만하면됩니다.

+0

예, 그게 내가하는 일입니다! :) 나는 프로덕션 환경에서 그런 코드를 사용하지 않을 것입니다. 그것은 단지 이론적 인 질문입니다. :) – Jasiu

0

__del__은 문제가 될 수 있습니다. 새 객체를 QUEUE_ITEM에 할당하기 전에 가비지 컬렉터가 이전 객체의 __del__ 메소드를 호출하지 못하게하는 방법 만 있다면 피할 수 있습니다. 우리는 다음과 같은 것이 필요할 것입니다 :

increase the reference counter on the old object 
assign a new one to `QUEUE_ITEM` 
decrease the reference counter on the old object 

나는 그것이 가능할 지 모르지만, 두려워요.