管程 - 生產者消費者問題
前言
上一篇介紹了信號量,PV,以及一個經典的同步問題(讀者寫者問題),如果還不了解的人,歡迎先去看看 PV 操作 - 讀者寫者問題。而在討論管程之前,我們再介紹一個經典的同步問題,生產者消費者問題,並用這個問題說明管程的概念和實現方式。
正文
生產者消費者問題
首先先說明下生產者消費者問題的規則:
系統中有一組生產者進程和一組消費者進程,生產者進程每次生產一個產品並放入緩衝區,消費者進程每次從緩衝區中取出一個產品並使用。生產者,消費者共用一個初始化為空,大小為 n 的緩衝區。
只有緩衝區沒滿的時候,生產者才能把產品放入緩衝區,否則必須等待。只有緩衝區不為空的時候,消費者才能從中取出產品,否則必須等待。
很明顯,我們可以知道,緩衝區正是進程間共享的資源,應該由臨界區保護。先複習下,我們來看看使用信號量要怎麼實現。
信號量實現 - 生產者消費者問題
semaphore mutex; // 實現對緩衝區的互斥訪問
semaphore empty; // 表示空閒緩衝區的數量
semaphore full; // 表示產品數量,也表示緩衝區當前占滿數
mutex.count = 1;
empty.count = n;
full.count = 0;
// 生產者進程結構
producer {
while(1) {
/* 生產一個產品 */
P(empty);
P(mutex);
/* 把產品放入緩衝區 */
V(mutex);
V(full);
}
}
// 消費者進程結構
consumer {
while(1) {
P(full);
P(mutex);
/* 從緩衝區拿走一個產品 */
V(empty);
V(mutex);
}
}
管程 (monitor)
為什麼需要管程 ?
信號量固然可以實現同步的問題,不過也注意到它的缺點。就是那麼我只要多一個消費者或是生產者,我就要加入相應的代碼,且稍有錯誤,都可能引起死鎖等問題,而通常這種錯誤很致命,因為必須是特定進程執行順序才會發生錯誤,會使程序變得非常難以維護。
管程的引入就是為了要解決這樣的問題,提高複用性和安全性。
管程概念
管程是 Dijkstra 提出的,可以理解為一種 ADT。抽象數據類型(簡稱 ADT)封裝了數據及對其操作的一組函數,這一類型獨立於任何特定的 ADT 實現。管程屬於 ADT 類型,提供一組由程序員自己定義,在管程內互斥的操作。
管程定義
一個管程的定義通常要包含以下部分:
- 管程內的共享數據結構
- 對該數據結構的一組操作(函數)
- 對該數結構初始化的語句
- 管程自己的名稱
管程基本規則
管程這種 ADT 也有一些規則,通常有如下幾點:

- 管程內的共享數據結構只能由管程內定義的操作(函數)訪問
- 一個進程只能通過調用管程內的函數才能訪問到管程內的共享數據結構
- 同時只允許一個進程在管程內
管程使用方法
管程包括一組變數,用於定義這一類型的實例狀態,其實就是上面提到過的共享數據結構,也包括操作這些變數的函數實現。管程類型的語法如下所示:
monitor monitor_name {
/* 共享數據結構 */
function P1(...) {
...
}
function P2(...) {
...
}
.
.
.
function Pn(...) {
...
}
initialization_code(...) {
...
}
}
管程確保每次只有一個進程在可以管程內處於活動狀態。因此,程序員不需要自己編寫同步約束,如下圖:
而在管程內定義共享數據時,我們使用一個叫做條件結構(condition) 的類型來定義,實現同步操作。
condition x, y;
對於條件變量,只有 P(wait) 或者 V(signal) 操作可以訪問。例如,假設今天有一個進程調用了一個管程函數,而內部有 wait(x)
這一操作,那該進程就會被掛起,直到另一進程執行了 signal(x)
操作。
管程實現 - 生產者消費者問題
由於重點在理解管程,這邊部分的代碼實現使用了偽代碼。
- 管程定義:
monitor Producer_Consumer {
// empty: 表示空閒緩衝區的數量
// full: 表示產品數量,也表示緩衝區當前占滿數
condition full, empty;
int count; // 緩衝區產品數
void add(Item item) {
if(count == n) {
wait(full);
}
count++;
add_Product(item); // 把產品放入緩衝區
if(count == 1) {
signal(empty);
}
}
void remove() {
if(count == 0) {
wait(empty);
}
count--;
remove_Product(); // 從緩衝區移出一個產品
if(count == n-1) {
signal(full);
}
}
void init() {
full = 0;
empty = n;
}
}
- 生產者進程結構
Producer {
while(1) {
Item item = make_item(); // 生產出一個產品
Producer_Consumer.add(item);
}
}
- 消費者進程結構
Consumer {
while(1) {
Producer_Consumer.remove();
}
}
結語
本篇藉由經典的生產者消費者問題,大致描述了管程這個 ADT 的概念和實現方式。第一次在讀這部分的資料時,感覺非常模糊,不過自己寫過一次博客後似乎清晰了不少。與這篇相關的內容我也有寫過兩篇,臨界區以及 Peterson 算法 / PV 操作 - 讀者寫者問題,歡迎參考。若本篇哪裡有誤,也歡迎各位指點小白!