Python中的生產者消費者問題

Kroderia發表於2013-12-04

我們將使用Python執行緒來解決Python中的生產者—消費者問題。這個問題完全不像他們在學校中說的那麼難。

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

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

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

當我們在使用執行緒時,你可以學習以下的執行緒概念:

  • Condition:執行緒中的條件。
  • wait():在條件例項中可用的wait()。
  • notify() :在條件例項中可用的notify()。

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

引用維基百科:

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

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

再次引用維基百科:

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

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

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

一開始帶bug的程式:

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

樣例輸出:

解釋:

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

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

什麼是正確行為?

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

關於條件

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

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

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

重寫消費者程式碼:

重寫生產者程式碼:

樣例輸出:

解釋:

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

為佇列增加大小限制

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

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

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

最終程式如下:

樣例輸出:

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

以下用Queue來更新一下程式碼。

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

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

更新程式:

解釋:

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

相關文章