Guarded Suspension模式:等待喚醒機制

話題:Guarded Suspension模式:等待喚醒機制的規範實現

前不久,同事小灰工作中遇到一個問題,他開發了一個Web項目:Web版的文件瀏覽器,通過它用戶可以在瀏覽器裏查看服務器上的目錄和文件。這個項目依賴運維部門提供的文件瀏覽服務,而這個文件瀏覽服務只支持消息隊列(MQ)方式接入。消息隊列在互聯網大廠中用的非常多,主要用作流量削峯和系統解耦。在這種接入方式中,發送消息和消費結果這兩個操作之間是異步的,你可以參考下面的示意圖來理解。
在這裏插入圖片描述
在小灰的這個Web項目中,用戶通過瀏覽器發過來一個請求,會被轉換成一個異步消息發送給MQ,等MQ返回結果後,再將這個結果返回至瀏覽器。小灰同學的問題是:給MQ發送消息的線程是處理Web請求的線程T1,但消費MQ結果的線程並不是線程T1,那線程T1如何等待MQ的返回結果呢?爲了便於你理解這個場景,將其代碼化了,示例代碼如下。

class Message{
  String id;
  String content;
}

//該方法可以發送消息
void send(Message msg){
  //省略相關代碼
}

//MQ消息返回後會調用該方法
//該方法的執行線程不同於發送消息的線程
void onMessage(Message msg){
  //省略相關代碼
}

//處理瀏覽器發來的請求
Respond handleWebReq(){

  //創建一消息
  Message msg1 = new Message("1","{...}");
  //發送消息
  send(msg1);
  
  //如何等待MQ返回的消息呢?傻傻的等待還機智的等待呢??
  String result = ...;
}

類似於異步轉同步問題,今天咱們再仔細聊聊這個問題,讓你知其所以然,遇到類似問題也能自己設計出方案來。

Guarded Suspension模式

上面小灰遇到的問題,在現實世界裏比比皆是,只是我們一不小心就忽略了。比如,項目組團建要外出聚餐,我們提前預訂了一個包間,然後興沖沖地奔過去,到那兒後大堂經理看了一眼包間,發現服務員正在收拾,就會告訴我們:“您預訂的包間服務員正在收拾,請您稍等片刻。”過了一會,大堂經理髮現包間已經收拾完了,於是馬上帶我們去包間就餐。

我們等待包間收拾完的這個過程和小灰遇到的等待MQ返回消息本質上是一樣的,都是等待一個條件滿足:就餐需要等待包間收拾完,小灰的程序裏要等待MQ返回消息。

那我們來看看現實世界裏是如何解決這類問題的呢?現實世界裏大堂經理這個角色很重要,我們是否等待,完全是由他來協調的。通過類比,相信你也一定有思路了:我們的程序裏,也需要這樣一個大堂經理。的確是這樣,那程序世界裏的大堂經理該如何設計呢?其實設計方案前人早就搞定了,而且還將其總結成了一個設計模式:Guarded Suspension。所謂Guarded Suspension,直譯過來就是“保護性地暫停”。那下面我們就來看看,Guarded Suspension模式是如何模擬大堂經理進行保護性地暫停的。

下圖就是Guarded Suspension模式的結構圖,非常簡單,一個對象GuardedObject,內部有一個成員變量——受保護的對象,以及兩個成員方法——get(Predicate<T> p)onChanged(T obj)方法。其中,對象GuardedObject就是我們前面提到的大堂經理,受保護對象就是餐廳裏面的包間;受保護對象的get()方法對應的是我們的就餐,就餐的前提條件是包間已經收拾好了,參數p就是用來描述這個前提條件的;受保護對象的onChanged()方法對應的是服務員把包間收拾好了,通過onChanged()方法可以fire一個事件,而這個事件往往能改變前提條件p的計算結果。下圖中,左側的綠色線程就是需要就餐的顧客,而右側的藍色線程就是收拾包間的服務員。

在這裏插入圖片描述
GuardedObject的內部實現非常簡單,是管程的一個經典用法,你可以參考下面的示例代碼,核心是:get()方法通過條件變量的await()方法實現等待,onChanged()方法通過條件變量的signalAll()方法實現喚醒功能。邏輯還是很簡單的,所以這裏就不再詳細介紹了。

class GuardedObject<T>{

  //受保護的對象
  T obj;
  
  final Lock lock = new ReentrantLock();
  final Condition done = lock.newCondition();
  final int timeout=1;
  
  //獲取受保護對象  
  T get(Predicate<T> p) {
    lock.lock();
    try {
      //MESA管程推薦寫法
      while(!p.test(obj)){
        done.await(timeout, TimeUnit.SECONDS);
      }
    }catch(InterruptedException e){
      throw new RuntimeException(e);
    }finally{
      lock.unlock();
    }
    //返回非空的受保護對象
    return obj;
  }
  
  //事件通知方法
  void onChanged(T obj) {
    lock.lock();
    try {
      this.obj = obj;
      done.signalAll();
    } finally {
      lock.unlock();
    }
  }
}

擴展Guarded Suspension模式

上面我們介紹了Guarded Suspension模式及其實現,這個模式能夠模擬現實世界裏大堂經理的角色,那現在我們再來看看這個“大堂經理”能否解決小灰同學遇到的問題。

Guarded Suspension模式裏GuardedObject有兩個核心方法,一個是get()方法,一個是onChanged()方法。很顯然,在處理Web請求的方法handleWebReq()中,可以調用GuardedObject的get()方法來實現等待;在MQ消息的消費方法onMessage()中,可以調用GuardedObject的onChanged()方法來實現喚醒。

//處理瀏覽器發來的請求
Respond handleWebReq(){
  //創建一消息
  Message msg1 = new Message("1","{...}");
  //發送消息
  send(msg1);
  
  //利用GuardedObject實現等待
  GuardedObject<Message> go =new GuardObjec<>();
  Message r = go.get(t->t != null);
}

void onMessage(Message msg){
  //如何找到匹配的go?
  GuardedObject<Message> go=???
  go.onChanged(msg);
}

但是在實現的時候會遇到一個問題,handleWebReq()裏面創建了GuardedObject對象的實例go,並調用其get()方等待結果,那在onMessage()方法中,如何才能夠找到匹配的GuardedObject對象呢?這個過程類似服務員告訴大堂經理某某包間已經收拾好了,大堂經理如何根據包間找到就餐的人。現實世界裏,大堂經理的頭腦中,有包間和就餐人之間的關係圖,所以服務員說完之後大堂經理立刻就能把就餐人找出來。

我們可以參考大堂經理識別就餐人的辦法,來擴展一下Guarded Suspension模式,從而使它能夠很方便地解決小灰同學的問題。在小灰的程序中,每個發送到MQ的消息,都有一個唯一性的屬性id,所以我們可以維護一個MQ消息id和GuardedObject對象實例的關係,這個關係可以類比大堂經理大腦裏維護的包間和就餐人的關係。

有了這個關係,我們來看看具體如何實現。下面的示例代碼是擴展Guarded Suspension模式的實現,擴展後的GuardedObject內部維護了一個Map,其Key是MQ消息id,而Value是GuardedObject對象實例,同時增加了靜態方法create()和fireEvent();create()方法用來創建一個GuardedObject對象實例,並根據key值將其加入到Map中,而fireEvent()方法則是模擬的大堂經理根據包間找就餐人的邏輯。

class GuardedObject<T>{
  //受保護的對象
  T obj;
  final Lock lock = new ReentrantLock();
  final Condition done =lock.newCondition();
  final int timeout=2;
  
  //保存所有GuardedObject
  final static Map<Object, GuardedObject> gos=new ConcurrentHashMap<>();
  
  //靜態方法創建GuardedObject
  static <K> GuardedObject create(K key){
    GuardedObject go = new GuardedObject();
    gos.put(key, go);
    return go;
  }
  
  static <K, T> void fireEvent(K key, T obj){
    GuardedObject go=gos.remove(key);
    if (go != null){
      go.onChanged(obj);
    }
  }
  
  //獲取受保護對象  
  T get(Predicate<T> p) {
    lock.lock();
    try {
      //MESA管程推薦寫法
      while(!p.test(obj)){
        done.await(timeout, TimeUnit.SECONDS);
      }
    }catch(InterruptedException e){
      throw new RuntimeException(e);
    }finally{
      lock.unlock();
    }
    //返回非空的受保護對象
    return obj;
  }
  
  //事件通知方法
  void onChanged(T obj) {
    lock.lock();
    try {
      this.obj = obj;
      done.signalAll();
    } finally {
      lock.unlock();
    }
  }
}

這樣利用擴展後的GuardedObject來解決小灰同學的問題就很簡單了,具體代碼如下所示。

//處理瀏覽器發來的請求
Respond handleWebReq(){

  int id=序號生成器.get();
  
  //創建一消息
  Message msg1 = new Message(id,"{...}");
  
  //創建GuardedObject實例
  GuardedObject<Message> go= GuardedObject.create(id);  
  //發送消息
  send(msg1);
  
  //等待MQ消息
  Message r = go.get(t->t != null);  
}
void onMessage(Message msg){
  //喚醒等待的線程
  GuardedObject.fireEvent(msg.id, msg);
}

總結

Guarded Suspension模式本質上是一種等待喚醒機制的實現,只不過Guarded Suspension模式將其規範化了。規範化的好處是你無需重頭思考如何實現,也無需擔心實現程序的可理解性問題,同時也能避免一不小心寫出個Bug來。但Guarded Suspension模式在解決實際問題的時候,往往還是需要擴展的,擴展的方式有很多,本篇文章就直接對GuardedObject的功能進行了增強,Dubbo中DefaultFuture這個類也是採用的這種方式,你可以對比着來看,相信對DefaultFuture的實現原理會理解得更透徹。當然,你也可以創建新的類來實現對Guarded Suspension模式的擴展。

Guarded Suspension模式也常被稱作Guarded Wait模式、Spin Lock模式(因爲使用了while循環去等待),這些名字都很形象,不過它還有一個更形象的非官方名字多線程版本的if。單線程場景中,if語句是不需要等待的,因爲在只有一個線程的條件下,如果這個線程被阻塞,那就沒有其他活動線程了,這意味着if判斷條件的結果也不會發生變化了。但是多線程場景中,等待就變得有意義了,這種場景下,if判斷條件的結果是可能發生變化的。所以,用“多線程版本的if”來理解這個模式會更簡單。

Demo

1
public class SuspensionClient {
    public static void main(String[] args) throws InterruptedException {

        final RequestQueue queue = new RequestQueue();
        new ClientThread(queue, "Alex").start();
        ServerThread serverThread = new ServerThread(queue);
        serverThread.start();
        //serverThread.join();

        Thread.sleep(10000);
        serverThread.close();
    }
}
2
public class RequestQueue {

    private final LinkedList<Request> queue = new LinkedList<>();

    public Request getRequest() {
        synchronized (queue) {
            while (queue.size() <= 0) {
                try {
                    queue.wait();
                } catch (InterruptedException e) {
                    return null;
                }
            }

            Request request = queue.removeFirst();
            return request;
        }
    }

    public void putRequest(Request request) {
        synchronized (queue) {
            queue.addLast(request);
            queue.notifyAll();
        }
    }
}
3
public class ClientThread extends Thread {

    private final RequestQueue queue;

    private final Random random;

    private final String sendValue;

    public ClientThread(RequestQueue queue, String sendValue) {
        this.queue = queue;
        this.sendValue = sendValue;
        random = new Random(System.currentTimeMillis());
    }

    @Override
    public void run() {
        for (int i = 0; i < 10; i++) {
            System.out.println("Client -> request " + sendValue);
            //put就會喚醒等待的
            queue.putRequest(new Request(sendValue));
            try {
                Thread.sleep(random.nextInt(1000));
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}
4
public class ServerThread extends Thread {

    private final RequestQueue queue;

    private final Random random;

    private volatile boolean closed = false;

    public ServerThread(RequestQueue queue) {
        this.queue = queue;
        random = new Random(System.currentTimeMillis());
    }

    @Override
    public void run() {
        while (!closed) {
            Request request = queue.getRequest();
            if (null == request) {
                System.out.println("Received the empty request.");
                continue;
            }
            System.out.println("Server ->" + request.getValue());
            try {
                Thread.sleep(random.nextInt(1000));
            } catch (InterruptedException e) {
                return;
            }
        }
    }

    /**
     * 這個方法作用是:巧妙的關閉了等待從RequestQueue獲取Request對象的線程
     * 類似於線程這樣重資源的使用需要注意要釋放資源
     */
    public void close() {
        this.closed = true;
        this.interrupt();//queue.wait();這裏可能一直阻塞了 this是當前線程
    }
}
5
public class Request {
    final private String value;

    public Request(String value) {
        this.value = value;
    }

    public String getValue() {
        return value;
    }
}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章