生產者-消費者(producer-consumer)問題,也稱作有界緩衝區(bounded-buffer)問題。兩個進程共享一個公共的固定大小的緩衝區。其中一個是生產者,將信息放入緩衝區;另一個是消費者,從緩衝區中取出信息。(也可以把這個問題一般化爲m個生產者和n個消費者問題,但是我們只討論一個生產者和一個消費者的情況,這樣可以簡化解決方案。)
問題在於當緩衝區已滿,而此時生產者還想向其中放入一個新的數據項的情況。其解決辦法是讓生產者睡眠,待消費者從緩衝區中取出一個或多個數據項時再喚醒它。同樣地,當消費者試圖從緩衝區中取數據而發現緩衝區爲空時,消費者就睡眠,直到生產者向其中放入一些數據時再將其喚醒。
這個方法聽起來很簡單,但它包含與前邊假脫機目錄問題一樣的競爭條件。爲了跟蹤緩衝區中的數據項數,我們需要一個變量count。如果緩衝區最多存放N個數據項,則生產者代碼將首先檢查count是否達到N,若是,則生產者睡眠;否則生產者向緩衝區中放入一個數據項並增量count的值。
消費者的代碼與此類似:首先測試count是否爲0,若是,則睡眠;否則從中取走一個數據項並遞減count的值。每個進程同時也檢測另一個進程是否應被喚醒,若是則喚醒之。生產者和消費者的代碼如圖所示。
爲了在C語言中表示sleep和wakeup這樣的系統調用,我們將以庫函數調用的形式來表示。儘管它們不是標準C庫的一部分,但在實際上任何系統中都具有這些庫函數。未列出的過程insert_item和remove_item用來記錄將數據項放入緩衝區和從緩衝區取出數據等事項。
現在回到競爭條件的問題。這裏有可能會出現競爭條件,其原因是對count的訪問未加限制。有可能出現以下情況:緩衝區爲空,消費者剛剛讀取count的值發現它爲0。此時調度程序決定暫停消費者並啓動運行生產者。生產者向緩衝區中加入一個數據項,count加1。現在count的值變成了1。它推斷認爲由於count剛纔爲0,所以消費者此時一定在睡眠,於是生產者調用wakeup來喚醒消費者。
但是,消費者此時在邏輯上並未睡眠,所以wakeup信號丟失。當消費者下次運行時,它將測試先前讀到的count值,發現它爲0,於是睡眠。生產者遲早會填滿整個緩衝區,然後睡眠。這樣一來,兩個進程都將永遠睡眠下去。
問題的實質在於發給一個(尚)未睡眠進程的wakeup信號丟失了。如果它沒有丟失,則一切都很正常。一種快速的彌補方法是修改規則,加上一個喚醒等待位。當一個wakeup信號發送給一個清醒的進程信號時,將該位置1。隨後,當該進程要睡眠時,如果喚醒等待位爲1,則將該位清除,而該進程仍然保持清醒。喚醒等待位實際上就是wakeup信號的一個小倉庫。
儘管在這個簡單例子中用喚醒等待位的方法解決了問題,但是我們很容易就可以構造出一些例子,其中有三個或更多的進程,這時一個喚醒等待位就不夠使用了。於是我們可以再打一個補丁,加入第二個喚醒等待位,甚至是8個、32個等,但原則上講,這並沒有從根本上解決問題。
/// @author zhaolu
/// @date 2020/04/16
#include <iostream> // std::cout std::endl
#include <atomic> // std::atomic_bool
#include <thread> // std::thread std::this_thread
std::atomic_bool flag(false);
int num = 10;
void produce() {
while (num)
{
while (flag != false)
std::this_thread::yield();
std::cout << "produce:" << num-- << std::endl;
flag = true;
}
}
void consume() {
while (num)
{
while (flag != true)
std::this_thread::yield();
std::cout << "consume:" << num + 1 << std::endl;
flag = false;
}
}
int main() {
std::thread producer (produce);
std::thread consumer (consume);
producer.join();
consumer.join();
return 0;
}
輸出結果爲:
:g++ -std=c++11 main.cc -o main
:./main
produce:10
consume:10
produce:9
consume:9
produce:8
consume:8
produce:7
consume:7
produce:6
consume:6
produce:5
consume:5
produce:4
consume:4
produce:3
consume:3
produce:2
consume:2
produce:1
consume:1
大多數應該在生產和消費的時候睡眠,然後指定條件下被喚醒。