【操作系統】原語之經典問題 - “生產者-消費者問題”

生產者-消費者(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

大多數應該在生產和消費的時候睡眠,然後指定條件下被喚醒。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章