管程(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_(synchronization)

相關文章