我們將使用Python執行緒來解決Python中的生產者—消費者問題。這個問題完全不像他們在學校中說的那麼難。
如果你對生產者—消費者問題有了解,看這篇部落格會更有意義。
為什麼要關心生產者—消費者問題:
- 可以幫你更好地理解併發和不同概念的併發。
- 資訊佇列中的實現中,一定程度上使用了生產者—消費者問題的概念,而你某些時候必然會用到訊息佇列。
當我們在使用執行緒時,你可以學習以下的執行緒概念:
- Condition:執行緒中的條件。
- wait():在條件例項中可用的wait()。
- notify() :在條件例項中可用的notify()。
我假設你已經有這些基本概念:執行緒、競態條件,以及如何解決靜態條件(例如使用lock)。否則的話,你建議你去看我上一篇文章basics of Threads。
引用維基百科:
生產者的工作是產生一塊資料,放到buffer中,如此迴圈。與此同時,消費者在消耗這些資料(例如從buffer中把它們移除),每次一塊。
這裡的關鍵詞是“同時”。所以生產者和消費者是併發執行的,我們需要對生產者和消費者做執行緒分離。
1 2 3 4 5 6 7 8 9 |
from threading import Thread class ProducerThread(Thread): def run(self): pass class ConsumerThread(Thread): def run(self): pass |
再次引用維基百科:
這個為描述了兩個共享固定大小緩衝佇列的程式,即生產者和消費者。
假設我們有一個全域性變數,可以被生產者和消費者執行緒修改。生產者產生資料並把它加入到佇列。消費者消耗這些資料(例如把它移出)。
1 |
queue = [] |
在剛開始,我們不會設定固定大小的條件,而在實際執行時加入(指下述例子)。
一開始帶bug的程式:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 |
from threading import Thread, Lock import time import 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結束執行。
樣例輸出:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
Produced 3 Consumed 3 Produced 4 Consumed 4 Produced 1 Consumed 1 Nothing in queue, but consumer will try to consume Exception 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正常工作。
重寫消費者程式碼:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
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()) |
重寫生產者程式碼:
1 2 3 4 5 6 7 8 9 10 11 12 |
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()) |
樣例輸出:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
Produced 3 Consumed 3 Produced 1 Consumed 1 Produced 4 Consumed 4 Produced 3 Consumed 3 Nothing in queue, consumer is waiting Produced 2 Producer added something to queue and notified the consumer Consumed 2 Nothing in queue, consumer is waiting Produced 2 Producer added something to queue and notified the consumer Consumed 2 Nothing in queue, consumer is waiting Produced 3 Producer added something to queue and notified the consumer Consumed 3 Produced 4 Consumed 4 Produced 1 Consumed 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然後往佇列中加入資料。
最終程式如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 |
from threading import Thread, Condition import time import random queue = [] MAX_NUM = 10 condition = 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() |
樣例輸出:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
Produced 0 Consumed 0 Produced 0 Produced 4 Consumed 0 Consumed 4 Nothing in queue, consumer is waiting Produced 4 Producer added something to queue and notified the consumer Consumed 4 Produced 3 Produced 2 Consumed 3 |
更新:
很多網友建議我在lock和condition下使用Queue來代替使用list。我同意這種做法,但我的目的是展示Condition,wait()和notify()如何工作,所以使用了list。
以下用Queue來更新一下程式碼。
Queue封裝了Condition的行為,如wait(),notify(),acquire()。
現在不失為一個好機會讀一下Queue的文件(http://docs.python.org/2/library/queue.html)。
更新程式:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 |
from threading import Thread import time import random from 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的原始碼吧。