管程(Monitor)概念及Java的實現原理
阿新 • • 發佈:2020-06-02
### 互斥
互斥訪問是併發程式設計要解決的核心問題之一。
有許多種方法可以滿足臨界區的互斥訪問。大體上可以分為三種,
一種是軟體方法,即由使用者程式承擔互斥訪問的責任,而不需要依賴程式語言或作業系統,譬如Dekker演算法、Peterson演算法等,通常這種方式會有一定的效能開銷和程式設計難度。
第二種是作業系統或程式語言對互斥的原生支援,譬如Linux中的mutex、Java語言的synchronized。
最後是硬體上的特殊指令,譬如著名的CAS。這種方式開銷最少,但是很難成為一種通用的解決方案,通常作業系統或程式語言的互斥是基於此建立起來的。
### 管程-Monitor
管程屬於程式語言級別的互斥解決方案,最早是Brinch Hanson和Hoare於1970s提出的概念,已在Pascal、Java、Python等語言中得到了實現。
“管程”一詞翻譯自英文`Monitor Procedures`,字面理解就是管理一個或多個執行過程。(但是個人感覺“管程”這個翻譯有點莫名其妙,看完更迷糊了,所以本文堅持用回原名Monitor。)
Monitor本質上是對`通用同步工具`的一種抽象,它就像一個執行緒安全的盒子,使用者程式把一個方法或過程(程式碼塊)放進去,它就可以為他們提供一種保障:**同一時刻只能有一個程序/執行緒執行該方法或過程,從而簡化了併發應用的開發難度**。
如果Monitor內沒有執行緒正在執行,則執行緒可以進入Monitor執行方法,否則該執行緒被放入`入口佇列`(entry queue)並使其掛起。當有執行緒從Monitor中退出時,會喚醒entry queue中的一個執行緒。
為了處理併發執行緒,Monitor還需要一個更基礎的同步工具,或者說需要一個機制,使得執行緒不僅被掛起,而且還能釋放Monitor,以便其他執行緒可以進入。
Monitor使用`條件變數`(Condition Variable)支援這種機制,這些條件變數(一個或多個)包含在Monitor中,並且只有在Monitor內才能被訪問 (類似Java物件的private變數)。
對外開放兩個方法以便使用者程式操作`條件變數`:
`cwait(c)`:呼叫該方法的執行緒在條件c上阻塞,monitor現在可以被其他執行緒使用。
`csignal(c)`:恢復在條件c上被阻塞的執行緒。若有多個這樣的執行緒,選擇其中一個。
(通常,為了保證cwait/csignal對條件變數的變更是原子性的,還需要藉助CAS)
### 當執行緒等待資源時
當Monitor中正在執行的執行緒無法獲取所需資源時,情況會變得更加複雜。
如果發生這種情況,等待資源的執行緒可以先把自己掛起,並且釋放Monitor的使用權,使得其他執行緒得以進入Monitor。
那麼問題來了,當第二個執行緒在執行期間,第一個執行緒所需的資源可用了,會發生什麼?
立即喚醒第一個執行緒,還是第二個執行緒先執行完?
對此產生了多個對Monitor的定義。
### Hoare版本
在Hoare的語義中,當資源可用時,ThreadA立即恢復執行,而ThreadB進入signal queue。
```
1.ThreadA 進入 monitor
2.ThreadA 等待資源 (進入wait queue)
3.ThreadB 進入monitor
4.ThreadB 資源可用 ,通知ThreadA恢復執行,並把自己轉移到signal queue。
5.ThreadA 重新進入 monitor
6.ThreadA 離開monitor
7.ThreadB 重新進入 monitor
8.ThreadB 離開monitor
9.其他在entry queue中的執行緒通過競爭進入monitor
```
### Mesa版本
在Mesa Monitor的實現中,第二個執行緒會先執行完。
ThreadA的資源可用時,把它從wait queue轉移到entry queue。ThreadB繼續執行至結束。
ThreadA最終也會從entry queue中得以執行。
```
1.ThreadA 進入 monitor
2.ThreadA 等待資源 (進入wait queue,並釋放monitor)
3.ThreadB 進入monitor
4.ThreadB 資源可用,通知ThreadA。(ThreadA被轉移到entey queue)
5.ThreadB 繼續執行
6.ThreadB 離開monitor
7.ThreadA 獲得執行機會,從entry queue出佇列,恢復執行
8.ThreadA 離開monitor
9.其他在entry queue中的執行緒通過競爭進入monitor
```
由於ThreadA被轉移到了entry queue,當ThreadB退出monitor後,ThreadA與其他執行緒平等競爭monitor的進入條件,所以並不能保證立即執行。
更不幸的是,等到ThreadA重入monitor後,資源可能再次不可用,重複以上過程。
### Brinch Hanson版本
Brinch Hanson Monitor(以下簡稱BH Monitor)只允許執行緒從monitor退出時發出訊號,此時被通知的執行緒進入monitor恢復執行。
```
1.ThreadA 進入 monitor
2.ThreadA 等待資源a
3.ThreadB 進入monitor
4.ThreadB 離開Monitor,並給通知等待資源a的執行緒,資源可用
5.ThreadA 重新進入 monitor
6.ThreadA 離開monitor
7.其他執行緒從entry queue中競爭進入monitor
```
###三種語義對比
Hoare Monitor中,資源可用時,ThreadB呼叫csignal()後被阻塞,以便ThreadA立即恢復執行。
這時ThreadB應該被放到哪裡?一種可能是轉移到entry queue,這樣它就必須與其他還未進入Montior的執行緒平等競爭獲取重入機會。
但是由於在呼叫csignal()之前,ThreadB已經執行了一部分,因此使它優先於其他執行緒是有意義的,
為此,Hoare Monitor增加了signal queue用於存放阻塞在csignal()上的執行緒。
Hoare Monitor的一個明顯缺點是,ThreadB在執行中途被中斷,需要額外的兩次執行緒切換才能恢復執行。
不同的是,Mesa Monitor和BH Monitor會保證ThreadB先執行完,因此不需要額外的signal queue。
### Java版本的Monitor
Java在實現時對最初的Monitor定義做了一些合理的限制。首先,與以上三種都不一樣的是,Java Montior只允許一個`條件變數`,而不是多個。
不像BH monitor,signal可以出現在程式碼的任何地方。
也不像Hoare monitor,資源可以時,被通知的執行緒不會立即執行,而是從BLOCK狀態變成RUNNABLE狀態,被CPU再次排程到時才恢復執行。
與cwait(c)和csignal(c)對應的是wait()和notify()方法。
Java monitor機制通過`synchronized`關鍵字暴露給使用者,syncronized可以使用者修飾方法或程式碼塊,兩者本質上都是一個執行過程。
###Java monitor實現生產者/消費者
```
//簡化版本,只允許一個生產者和一個消費者
class BoundedBuffer {
private int numSlots = 0;
private double[] buffer = null;
private int putIn = 0, takeOut = 0;
private int count = 0;
public BoundedBuffer(int numSlots) {
if (numSlots <= 0) throw new IllegalArgumentException("numSlots<=0");
this.numSlots = numSlots;
buffer = new double[numSlots];
System.out.println("BoundedBuffer alive, numSlots=" + numSlots);
}
public synchronized void deposit(double value) {
while (count == numSlots)
try {
wait();
} catch (InterruptedException e) {
System.err.println("interrupted out of wait");
}
buffer[putIn] = value;
putIn = (putIn + 1) % numSlots;
count++;
if (count == 1) notify(); //喚醒等待的consumer
}
public synchronized double fetch() {
double value;
while (count == 0)
try {
wait();
} catch (InterruptedException e) {
System.err.println("interrupted out of wait");
}
value = buffer[takeOut];
takeOut = (takeOut + 1) % numSlots;
count--; // wake up the producer
if (count == numSlots-1) notify(); // 喚醒等待的producer
return value;
}
}
```
> 1.Monitors and Condition Variables:https://cseweb.ucsd.edu/classes/sp17/cse120-a/applications/ln/lecture8.html
> 2.《作業系統精髓與設計原理》第五章
> 3.https://en.m.wikipedia.org/wiki/Monitor_(synchroni