用Python的線程來解決生產者消費問題的樣本

來源:互聯網
上載者:User
我們將使用Python線程來解決Python中的生產者—消費者問題。這個問題完全不像他們在學校中說的那麼難。

如果你對生產者—消費者問題有瞭解,看這篇部落格會更有意義。

為什麼要關心生產者—消費者問題:

  • 可以幫你更好地理解並發和不同概念的並發。
  • 資訊隊列中的實現中,一定程度上使用了生產者—消費者問題的概念,而你某些時候必然會用到訊息佇列。

當我們在使用線程時,你可以學習以下的線程概念:

  • Condition:線程中的條件。
  • wait():在條件執行個體中可用的wait()。
  • notify() :在條件執行個體中可用的notify()。

我假設你已經有這些基本概念:線程、競態條件,以及如何解決靜態條件(例如使用lock)。否則的話,你建議你去看我上一篇文章basics of Threads。

引用維基百科:

生產者的工作是產生一塊資料,放到buffer中,如此迴圈。與此同時,消費者在消耗這些資料(例如從buffer中把它們移除),每次一塊。

這裡的關鍵詞是“同時”。所以生產者和消費者是並發啟動並執行,我們需要對生產者和消費者做線程分離。

from threading import Thread class ProducerThread(Thread):  def run(self):    pass class ConsumerThread(Thread):  def run(self):    pass

再次引用維基百科:

這個為描述了兩個共用固定大小緩衝隊列的進程,即生產者和消費者。

假設我們有一個全域變數,可以被生產者和消費者線程修改。生產者產生資料並把它加入到隊列。消費者消耗這些資料(例如把它移出)。

queue = []

在剛開始,我們不會設定固定大小的條件,而在實際運行時加入(指下述例子)。

一開始帶bug的程式:

from threading import Thread, Lockimport timeimport random queue = []lock = Lock() class ProducerThread(Thread):  def run(self):    nums = range(5) #Will create the list [0, 1, 2, 3, 4]    global queue    while True:      num = random.choice(nums) #Selects a random number from list [0, 1, 2, 3, 4]      lock.acquire()      queue.append(num)      print "Produced", num      lock.release()      time.sleep(random.random()) class ConsumerThread(Thread):  def run(self):    global queue    while True:      lock.acquire()      if not queue:        print "Nothing in queue, but consumer will try to consume"      num = queue.pop(0)      print "Consumed", num      lock.release()      time.sleep(random.random()) ProducerThread().start()ConsumerThread().start()

運行幾次並留意一下結果。如果程式在IndexError異常後並沒有自動結束,用Ctrl+Z結束運行。

範例輸出:

Produced 3Consumed 3Produced 4Consumed 4Produced 1Consumed 1Nothing in queue, but consumer will try to consumeException in thread Thread-2:Traceback (most recent call last): File "/usr/lib/python2.7/threading.py", line 551, in __bootstrap_inner  self.run() File "producer_consumer.py", line 31, in run  num = queue.pop(0)IndexError: pop from empty list

解釋:

  • 我們開始了一個生產者線程(下稱生產者)和一個消費者線程(下稱消費者)。
  • 生產者不停地添加(資料)到隊列,而消費者不停地消耗。
  • 由於隊列是一個共用變數,我們把它放到lock程式塊內,以防發生競態條件。
  • 在某一時間點,消費者把所有東西消耗完畢而生產者還在掛起(sleep)。消費者嘗試繼續進行消耗,但此時隊列為空白,出現IndexError異常。
  • 在每次運行過程中,在發生IndexError異常之前,你會看到print語句輸出”Nothing in queue, but consumer will try to consume”,這是你出錯的原因。

我們把這個實現作為錯誤行為(wrong behavior)。

什麼是正確行為?

當隊列中沒有任何資料的時候,消費者應該停止運行並等待(wait),而不是繼續嘗試進行消耗。而當生產者在隊列中加入資料之後,應該有一個渠道去告訴(notify)消費者。然後消費者可以再次從隊列中進行消耗,而IndexError不再出現。

關於條件

條件(condition)可以讓一個或多個線程進入wait,直到被其他線程notify。參考:?http://docs.python.org/2/library/threading.html#condition-objects

這就是我們所需要的。我們希望消費者在隊列為空白的時候wait,只有在被生產者notify後恢複。生產者只有在往隊列中加入資料後進行notify。因此在生產者notify後,可以確保隊列非空,因此消費者消費時不會出現異常。

  • condition內含lock。
  • condition有acquire()和release()方法,用以調用內部的lock的對應方法。

condition的acquire()和release()方法內部調用了lock的acquire()和release()。所以我們可以用condiction執行個體取代lock執行個體,但lock的行為不會改變。
生產者和消費者需要使用同一個condition執行個體, 保證wait和notify正常工作。

重寫消費者代碼:

from threading import Condition condition = Condition() class ConsumerThread(Thread):  def run(self):    global queue    while True:      condition.acquire()      if not queue:        print "Nothing in queue, consumer is waiting"        condition.wait()        print "Producer added something to queue and notified the consumer"      num = queue.pop(0)      print "Consumed", num      condition.release()      time.sleep(random.random())

重寫生產者代碼:

class ProducerThread(Thread):  def run(self):    nums = range(5)    global queue    while True:      condition.acquire()      num = random.choice(nums)      queue.append(num)      print "Produced", num      condition.notify()      condition.release()      time.sleep(random.random())

範例輸出:

Produced 3Consumed 3Produced 1Consumed 1Produced 4Consumed 4Produced 3Consumed 3Nothing in queue, consumer is waitingProduced 2Producer added something to queue and notified the consumerConsumed 2Nothing in queue, consumer is waitingProduced 2Producer added something to queue and notified the consumerConsumed 2Nothing in queue, consumer is waitingProduced 3Producer added something to queue and notified the consumerConsumed 3Produced 4Consumed 4Produced 1Consumed 1

解釋:

  • 對於消費者,在消費前檢查隊列是否為空白。
  • 如果為空白,調用condition執行個體的wait()方法。
  • 消費者進入wait(),同時釋放所持有的lock。
  • 除非被notify,否則它不會運行。
  • 生產者可以acquire這個lock,因為它已經被消費者release。
  • 當調用了condition的notify()方法後,消費者被喚醒,但喚醒不意味著它可以開始運行。
  • notify()並不釋放lock,調用notify()後,lock依然被生產者所持有。
  • 生產者通過condition.release()顯式釋放lock。
  • 消費者再次開始運行,現在它可以得到隊列中的資料而不會出現IndexError異常。

為隊列增加大小限制

生產者不能向一個滿隊列繼續加入資料。

它可以用以下方式來實現:

  • 在加入資料前,生產者檢查隊列是否為滿。
  • 如果不為滿,生產者可以繼續正常流程。
  • 如果為滿,生產者必須等待,調用condition執行個體的wait()。
  • 消費者可以運行。消費者消耗隊列,併產生一個空餘位置。
  • 然後消費者notify生產者。
  • 當消費者釋放lock,消費者可以acquire這個lock然後往隊列中加入資料。

最終程式如下:

from threading import Thread, Conditionimport timeimport random queue = []MAX_NUM = 10condition = Condition() class ProducerThread(Thread):  def run(self):    nums = range(5)    global queue    while True:      condition.acquire()      if len(queue) == MAX_NUM:        print "Queue full, producer is waiting"        condition.wait()        print "Space in queue, Consumer notified the producer"      num = random.choice(nums)      queue.append(num)      print "Produced", num      condition.notify()      condition.release()      time.sleep(random.random()) class ConsumerThread(Thread):  def run(self):    global queue    while True:      condition.acquire()      if not queue:        print "Nothing in queue, consumer is waiting"        condition.wait()        print "Producer added something to queue and notified the consumer"      num = queue.pop(0)      print "Consumed", num      condition.notify()      condition.release()      time.sleep(random.random()) ProducerThread().start()ConsumerThread().start()

範例輸出:

Produced 0Consumed 0Produced 0Produced 4Consumed 0Consumed 4Nothing in queue, consumer is waitingProduced 4Producer added something to queue and notified the consumerConsumed 4Produced 3Produced 2Consumed 3

更新:
很多網友建議我在lock和condition下使用Queue來代替使用list。我同意這種做法,但我的目的是展示Condition,wait()和notify()如何工作,所以使用了list。

以下用Queue來更新一下代碼。

Queue封裝了Condition的行為,如wait(),notify(),acquire()。

現在不失為一個好機會讀一下Queue的文檔(http://docs.python.org/2/library/queue.html)。

更新程式:

from threading import Threadimport timeimport randomfrom Queue import Queue queue = Queue(10) class ProducerThread(Thread):  def run(self):    nums = range(5)    global queue    while True:      num = random.choice(nums)      queue.put(num)      print "Produced", num      time.sleep(random.random()) class ConsumerThread(Thread):  def run(self):    global queue    while True:      num = queue.get()      queue.task_done()      print "Consumed", num      time.sleep(random.random()) ProducerThread().start()ConsumerThread().start()

解釋:

  • 在原來使用list的位置,改為使用Queue執行個體(下稱隊列)。
  • 這個隊列有一個condition,它有自己的lock。如果你使用Queue,你不需要為condition和lock而煩惱。
  • 生產者調用隊列的put方法來插入資料。
  • put()在插入資料前有一個擷取lock的邏輯。
  • 同時,put()也會檢查隊列是否已滿。如果已滿,它會在內部調用wait(),生產者開始等待。
  • 消費者使用get方法。
  • get()從隊列中移出資料前會擷取lock。
  • get()會檢查隊列是否為空白,如果為空白,消費者進入等待狀態。
  • get()和put()都有適當的notify()。現在就去看Queue的源碼吧。
  • 聯繫我們

    該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

    如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

    A Free Trial That Lets You Build Big!

    Start building with 50+ products and up to 12 months usage for Elastic Compute Service

    • Sales Support

      1 on 1 presale consultation

    • After-Sales Support

      24/7 Technical Support 6 Free Tickets per Quarter Faster Response

    • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.