讀者-寫者問題是另一個里程碑式的同步互斥問題。它比生產者-消費者更復雜,因為它引入了不對稱的訪問權限:讀者和讀者之間是共享的,但寫者和任何人(包括讀者和其他寫者)之間都是互斥的。
我們用一個生動的比喻來解析這個經典問題:一個公共閱覽室里的一本珍貴孤本。
- 共享文件:閱覽室里唯一的一本珍貴孤本。
- 讀者進程:只想閱讀這本書的人。他們只是看,不會損壞書。
- 寫者進程:想要修訂或批注這本書的人。他們的操作會改變書的內容。
1. 規則分析:閱覽室的“規章制度”
為了保護這本孤本,閱覽室管理員制定了以下四條嚴格的規定:
- 允許多個讀者同時讀:只要書沒被修訂,可以有很多個讀者圍在一起同時看。這不影響什么。
- 只允許一個寫者寫:任何時候,最多只能有一個修訂者在書上寫字。
- 寫者工作時,不許任何人打擾:如果一個修訂者正在工作,那么任何讀者都不能來看,任何其他修訂者也不能來寫。必須保證修訂過程的絕對獨立。
- 有讀者在看時,寫者必須等待:如果已經有一群讀者在看書,那么新來的修訂者必須在門外等待,直到所有讀者都離開。
2. 初步嘗試:最簡單的“一刀切”方案
最簡單的管理辦法就是:閱覽室一次只許進一個人,不管你是讀者還是寫者。
- 實現:設置一個互斥信號量?
rw
,初始值為1。P(rw)
:進門前申請。V(rw)
:出門后歸還。
- 問題:這個方案雖然安全,但效率極低。它違反了規則1,明明可以多個讀者一起看,現在卻變成了讀者也要一個個排隊。這大大降低了閱覽室的利用率。
3. 核心突破:引入“讀者計數器”
為了實現“允許多個讀者同時讀”,我們需要知道當前有多少個讀者在閱覽室里。
- 思路:我們引入一個整數變量?
count
,初始為0,用來記錄讀者數量。- 第一個讀者到來時,他有特殊的責任:他需要負責把閱覽室的門鎖上,防止任何寫者進來。
- 后續的讀者到來時,發現門已經被第一個讀者鎖了(為讀者們鎖的),他們只需要把?
count
?加一,然后直接進去就行。 - 讀者離開時,也需要把?
count
?減一。 - 最后一個讀者離開時,他也有特殊的責任:他需要負責把閱覽室的門打開,好讓在外面等待的寫者能有機會進來。
4. 再次碰壁:計數器本身的“互斥”問題
上面的思路很好,但在并發環境下,對?count
?的“讀-改-寫”操作(比如?if(count==0)
?然后?count++
)不是原子的!
場景:
- 讀者A執行?
if(count==0)
,發現條件成立。 - 此時發生切換,讀者B也執行?
if(count==0)
,發現條件也成立。 - 結果:A和B都認為自己是“第一個”讀者,他們倆都去執行了“鎖門”操作(
P(rw)
)。第一個人鎖成功了,第二個人就會被永遠鎖在門外。
- 讀者A執行?
解決方案:引入一個新的互斥信號量?
mutex
,初始值為1,專門用來保護對?count
?變量的訪問。任何想修改?count
?的人,都得先拿到?mutex
?這把“鑰匙”。
5. “讀者優先”的解決方案(會導致寫者餓死)
結合以上思路,我們得到了第一個可行的、但有缺陷的方案。
semaphore rw = 1; // 用于實現讀寫互斥,也叫“閱覽室大門鎖”
semaphore mutex = 1; // 用于保護count變量的互斥鎖
int count = 0; // 讀者計數器reader() {P(mutex); // 1. 鎖住計數器,準備修改if (count == 0) { // 2. 如果我是第一個讀者P(rw); // 就負責鎖上閱覽室大門,阻止寫者}count++; // 3. 讀者數量加一V(mutex); // 4. 解鎖計數器// --- 臨界區 ---讀文件...// --- 臨界區 ---P(mutex); // 5. 鎖住計數器,準備修改count--; // 6. 讀者數量減一if (count == 0) { // 7. 如果我是最后一個讀者V(rw); // 就負責打開閱覽室大門,允許寫者進入}V(mutex); // 8. 解鎖計數器
}writer() {P(rw); // 1. 鎖上閱覽室大門// --- 臨界區 ---寫文件...// --- 臨界區 ---V(rw); // 2. 打開閱覽室大門
}
- 問題所在(寫者餓死):
- 假設一個寫者正在等待?
P(rw)
。此時,閱覽室里至少有一個讀者(count>0
),所以?rw
?鎖被占著。 - 如果在這個時候,不斷有新的讀者到來。他們可以成功通過?
P(mutex)
,把?count
?從1變成2,從2變成3... 他們根本不需要去碰那把被寫者等待的?rw
?鎖。 - 結果就是,只要有任何一個讀者在閱覽室里,后續源源不斷的讀者流都可以直接進入,而那個可憐的寫者,就永遠等不到?
count
?變成0的那一刻,永遠也進不了門。這就是“讀者優先”導致的“寫者餓死”。
- 假設一個寫者正在等待?
6. 最終解決方案:引入“寫者優先”機制(讀寫公平法)
為了解決寫者餓死的問題,我們需要一個機制,當一個寫者在等待時,后續新來的讀者不能“插隊”。
- 思路:我們再增加一把“門外的大門鎖”?
w
,初始值為1。- 寫者想寫時,先鎖上這把?
w
?鎖。 - 讀者想讀時,也得先嘗試獲取?
w
?鎖。
- 寫者想寫時,先鎖上這把?
- 這樣,一旦一個寫者在等待(即他已經成功執行了
P(w)
,但在等P(rw)
),那么后續所有新來的讀者都會被?P(w)
?擋在門外,無法插隊。他們只能等這個寫者完成工作,釋放了?w
?鎖之后,才能和其他等待的讀者一起公平競爭。
最終的、讀寫相對公平的解決方案:
semaphore rw = 1;
semaphore mutex = 1;
semaphore w = 1; // 新增的“寫者優先”鎖
int count = 0;reader() {P(w); // (新增) 在讀者隊列外再加一道關卡P(mutex);if (count == 0) {P(rw);}count++;V(mutex);V(w); // (新增) 讀者進入后立刻釋放w,允許其他讀者或寫者排隊讀文件...P(mutex);count--;if (count == 0) {V(rw);}V(mutex);
}writer() {P(w); // 1. 寫者優先獲取w鎖P(rw); // 2. 再獲取文件鎖寫文件...V(rw);V(w); // 3. 寫完后,釋放兩把鎖
}
- 效果分析:
- 這個方案并非絕對的“寫者優先”,因為它不會搶占已經在讀的讀者。
- 它實現的是一種相對公平的排隊機制。當一個寫者開始排隊(執行
P(w)
)后,后續新來的讀者也必須排在他后面,等待?w
?鎖。這保證了先來后到的公平性,避免了寫者饑餓。因此也被稱為讀寫公平法。
現在,我們來對這個被稱為“讀寫公平法”的最終解決方案,進行一次詳細、清晰的流程化文字介紹。
算法名稱
讀寫公平法?(亦常被某些教材描述為一種“寫者優先”的實現思路,但其效果更接近于公平排隊)。
算法目標
在解決讀者-寫者問題的基礎上,額外解決“讀者優先”方案中可能出現的寫者饑餓問題。其核心思想是:當一個寫者已經表示了想要寫入的意圖后,后續新到達的讀者不能“插隊”搶先進入,必須等待該寫者完成操作。
所需的信號量和變量
int count = 0;
- 角色:讀者計數器。
- 作用:記錄當前正在讀取共享文件的讀者進程數量。
- 初始值:0,表示初始時沒有讀者。
semaphore mutex = 1;
- 角色:互斥信號量。
- 作用:專門用于保護共享變量?
count
?的訪問。確保對?count
?的檢查和修改操作是原子的,防止多個讀者并發修改?count
?時出錯。 - 初始值:1,表示?
count
?的“修改權”可用。
semaphore rw = 1;
- 角色:互斥信號量。
- 作用:用于實現讀者與寫者之間、寫者與寫者之間的互斥。可理解為共享文件本身的“讀寫鎖”。一旦被加鎖,只有持有鎖的進程(或一群讀者)可以訪問文件。
- 初始值:1,表示文件當前可供訪問。
semaphore w = 1;
- 角色:同步/互斥信號量(關鍵所在)。
- 作用:這是一個實現“公平排隊”的關鍵信號量。它可以被看作是一個在所有讀者和寫者之上的、更高級別的“通行證”。它保證了當一個寫者正在等待時,后續的讀者無法越過它。
- 初始值:1,表示“通行證”可用。
讀者進程的詳細執行流程
一個讀者進程想要讀取文件,需要執行以下步驟:
申請“排隊資格” (
P(w)
):- 在嘗試讀取之前,首先要申請
w
這個“通行證”。如果此時有一個寫者正在寫或正在等待,那么w
鎖很可能已經被占用,該讀者就會在此處被阻塞,進入一個統一的等待隊列。這一步確保了讀者不會插隊到已在等待的寫者前面。
- 在嘗試讀取之前,首先要申請
鎖住計數器 (
P(mutex)
):- 成功通過
w
關卡后,為了安全地修改讀者計數器count
,進程需要先獲取mutex
鎖。
- 成功通過
判斷并鎖住文件(如果是第一個讀者):
- 檢查
count
的值。如果count == 0
,說明自己是當前第一個到達的讀者。作為“先鋒”,它有責任通過執行P(rw)
來鎖住共享文件,以阻止任何寫者進入。
- 檢查
更新計數器 (
count++
):- 將
count
加一,表明現在閱覽室里又多了一位讀者。
- 將
解鎖計數器 (
V(mutex)
):- 對
count
的修改已經完成,立刻釋放mutex
鎖,以便其他(已通過w
關卡的)讀者可以進來更新count
。
- 對
釋放“排隊資格” (
V(w)
):- 這是非常關鍵的一步。在進入讀操作之前,讀者會立刻釋放
w
鎖。這使得在它自己正在讀書時,其他進程(無論是讀者還是寫者)可以繼續競爭w
鎖并排隊。如果不釋放,就會變成一次只允許一個進程(或一批讀者)通過,大大降低并發性。
- 這是非常關鍵的一步。在進入讀操作之前,讀者會立刻釋放
執行讀操作 (
reading is performed
):- 這是讀者的臨界區。此時,文件鎖
rw
已經被第一個讀者鎖上,可以安全地讀取文件,并且允許多個讀者同時處于這個階段。
- 這是讀者的臨界區。此時,文件鎖
鎖住計數器(準備離開) (
P(mutex)
):- 讀完后,準備離開。再次獲取
mutex
鎖,以安全地修改count
。
- 讀完后,準備離開。再次獲取
更新計數器 (
count--
):- 將
count
減一,表明有一位讀者離開了。
- 將
判斷并解鎖文件(如果是最后一個讀者):
- 檢查
count
的值。如果count == 0
,說明自己是最后一個離開的讀者。作為“殿后”者,它有責任通過執行V(rw)
來解開文件鎖,以便在外等待的寫者可以進入。
- 檢查
解鎖計數器 (
V(mutex)
):- 對
count
的修改完成,釋放mutex
鎖。讀者進程的整個流程結束。
- 對
寫者進程的詳細執行流程
一個寫者進程想要寫入文件,流程相對簡單,但權力更大:
申請“排隊資格” (
P(w)
):- 與讀者一樣,寫者首先也需要獲取
w
這個“通行證”。一旦它成功獲取了w
(或正在等待w
),就能有效阻止新來的讀者插隊。
- 與讀者一樣,寫者首先也需要獲取
鎖住文件 (
P(rw)
):- 獲取了
w
之后,接著申請對文件的“獨占寫權限”,即rw
鎖。此時,如果仍有讀者在文件內(即rw
鎖被讀者們持有),寫者會在此處被阻塞,直到最后一個讀者離開并執行V(rw)
。
- 獲取了
執行寫操作 (
writing is performed
):- 這是寫者的臨界區。此時,寫者同時持有了
w
鎖和rw
鎖,保證了沒有任何其他讀者或寫者可以進入。
- 這是寫者的臨界區。此時,寫者同時持有了
解鎖文件 (
V(rw)
):- 寫操作完成,首先釋放文件鎖
rw
。
- 寫操作完成,首先釋放文件鎖
釋放“排隊資格” (
V(w)
):- 最后釋放
w
鎖,允許在w
上等待的其他進程(可能是讀者也可能是寫者)繼續競爭。
- 最后釋放
通過這一套精密的、由多個信號量協同工作的流程,該算法在保證數據一致性的前提下,既允許多個讀者并發讀取,又通過一個公平的排隊機制,有效避免了寫者進程被餓死的問題。
必會題與詳解
題目一:在“讀者優先”的解決方案中,互斥信號量mutex
的作用是什么?如果去掉它會發生什么?
答案詳解:
mutex
的作用:互斥信號量?mutex
?在這里的作用是保護共享變量?count
?的訪問。對?count
?的操作,如“檢查?count
?是否為0”和“count++
”,在邏輯上必須是一個原子操作。P(mutex)
?和?V(mutex)
?將這兩步操作捆綁在一起,確保在任何時刻只有一個讀者能修改?count
?的值。去掉
mutex
的后果:如果去掉?mutex
,兩個讀者進程可能會并發地執行對?count
?的修改,導致?count
?值不正確,并可能引發死鎖或互斥失效。- 一個典型的錯誤場景:
- 初始時?
count = 0
。 - 讀者A執行?
if (count == 0)
,判斷為真。 - 此時發生進程切換,讀者B執行?
if (count == 0)
,判斷也為真。 - 讀者A繼續執行,執行?
P(rw)
?成功,然后?count++
,count
?變為1。 - 切換回讀者B,它也執行?
P(rw)
。但此時?rw
?已經被A鎖住,因此讀者B被永久阻塞在了?P(rw)
?這里,因為它錯誤地認為自己是第一個讀者,而去嘗試獲取一個已經被占用的鎖。
- 初始時?
- 一個典型的錯誤場景:
題目二:在最終的“讀寫公平”解決方案中,新增的信號量?w
?是如何解決寫者饑餓問題的?
答案詳解:
信號量?w
?解決寫者饑餓問題的核心機制是建立了一個在所有讀者和寫者之上的、統一的“排隊關卡”。
形成排隊:無論是讀者還是寫者,在真正嘗試訪問文件(或?
count
?變量)之前,都必須先通過?P(w)
?這一關。這相當于在閱覽室的大門口設置了一個取號機,保證了先來后到的基本順序。阻止讀者插隊:
- 當沒有寫者等待時,
w
?鎖是開著的,讀者可以自由通過。 - 當一個寫者到來并開始等待時,它會首先執行?
P(w)
?并成功獲取?w
?鎖(或者被阻塞在?w
?上)。一旦?w
?鎖被某個寫者持有或等待,任何后續新來的讀者在執行它們自己的?P(w)
?時,都會被阻塞。 - 這就阻止了讀者無限插隊的情況。新來的讀者必須等到當前正在等待的寫者(以及在他之前排隊的進程)完成操作、釋放了?
w
?鎖之后,才有機會進入。
- 當沒有寫者等待時,
實現公平:通過這種方式,
w
?保證了當一個寫者已經“掛號”等待后,系統不會無視他而去服務那些后來的讀者。這大大提高了寫者被服務的機會,避免了饑餓,實現了讀寫進程間相對公平的競爭。
題目三:讀者-寫者問題和生產者-消費者問題在本質上有什么不同?
答案詳解:
兩者都是經典的同步互斥問題,但它們處理的“關系”有本質的不同。
關系對稱性不同:
- 生產者-消費者問題是對稱的。生產者之間是互斥的,消費者之間也是互斥的(因為它們都要操作同一個緩沖區指針或計數器),生產者和消費者之間也是互斥的。所有進程對緩沖區的訪問都是完全互斥的。
- 讀者-寫者問題是不對稱的。讀者和讀者之間是共享的(可以并發),而寫者與讀者、寫者與寫者之間都是互斥的。這種不對稱性使得其邏輯比前者復雜得多。
解決核心不同:
- 生產者-消費者的核心是資源(產品和空位)的計數與同步。它主要通過兩個同步信號量(
full
?和?empty
)來協調生產者和消費者的步調,防止從空緩沖區取或向滿緩沖區放。 - 讀者-寫者的核心是身份識別與權限管理。它需要區分進程是“讀者”還是“寫者”,并根據身份賦予不同的訪問權限。其解決方案的核心是一個?
count
?計數器,用來動態地判斷“第一個讀者”和“最后一個讀者”,從而實現復雜的、有條件的加鎖和解鎖。
- 生產者-消費者的核心是資源(產品和空位)的計數與同步。它主要通過兩個同步信號量(