Guarded Suspension

January 10, 2022

有時你會希望執行緒呼叫物件方法,而物件狀態尚未準備好,執行流程必須暫停,這應該不難,以 Java 為例,可以呼叫 wait 方法,讓執行緒暫停,進入等待集(wait set),問題在於誰負責檢查狀態、要求執行緒等待?

Semaphore

視情境而定,你可能決定由物件本身檢查自身狀態,若處於未準備好的狀態,要求執行緒等待,例如,有些場所會設置容流人數燈號,每進入一人就數字減一,離開一人數字加一,燈號物件負責檢查數字,若數字為 0 就不能進場,根據這個需求,來設計一個 Semaphore

class Semaphore {
    private int value;
    
    Semaphore(int value) {
        this.value = value;
    }
    
    synchronized void acquire() {
        while(value == 0) {
            try {
                wait();
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        }
        value--;
    }
    
    synchronized void release() {
        value++;
        if(value == 1) {
            notifyAll();
        }
    }
}

建立 Semaphore 可指定燈號最大數值,每呼叫一次 acquire,數值減一,在數值 0 時,若呼叫了acquire,執行緒就會進入等待,每呼叫一次 release,數值加一,如果這時數值為 1,通知等待中的執行緒。

來用這個 Semaphore 模擬一下客人的進場、離場:

static void pause(int millis) {
    try {
        Thread.sleep(millis);
    } catch (InterruptedException e) {
        throw new RuntimeException(e);
    }
}

public static void main(String[] args) {
    var MAX = 5;        
    var semaphore = new Semaphore(MAX);
    var random = new Random();

    for(var n = 0; n < 20; n++) {
        pause(random.nextInt(0, 5) * 10);
        var g = n;
        new Thread(() -> {
            while(true) {
                semaphore.acquire();
                System.out.println(g + "進場");
                pause(random.nextInt(0, 10) * 100);

                semaphore.release();
                System.out.println(g + "離場");
            }
        }).start();
    }
}

若程式語言環境提供執行緒功能,標準 API 可能就內建了 Semaphore,例如 Java 就有 java.util.concurrent.Semaphore

Barrier

如果物件本身負責檢查狀態、要求執行緒等待,以上這類的實現概念,可以使用 Guarded Suspension 這個名稱來作為溝通,另一個常見案例是 Barrier。

Barrier 的意思是「柵欄」,也就是設定柵欄並指定數量,如果有執行緒先來到這個柵欄,必須等待其他執行緒也來到這個柵欄,直到指定的執行緒數量達到,全部執行緒才能繼續往下執行。

來個簡單的 Barrier 實作:

class Barrier {
    private int value;
    private int count;
    
    Barrier(int value) {
        this.value = value;
    }
    
    synchronized void await() {
        count++;
        while(count < value) {
            try {
                wait();
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        }
        notifyAll();
    }
}

一個應用場景是,若希望伺服端執行緒與客戶端執行緒都必須準備就緒,才能繼續往下執行的話,可以如下:

var MAX = 2;        
var barrier = new Barrier(MAX);

var server = new Thread(() -> {
    barrier.await();
    System.out.println("Server go!");
});

var client = new Thread(() -> {
    barrier.await();
    System.out.println("Client go!");
});

server.start();
client.start();

就 Java 標準 API 而言,是有個 java.util.concurrent.CyclicBarrier 可以直接使用;基本上,多執行緒環境的資源控管,不建議自己搞,畢竟考量的因素很多,以上只是簡單示範罷了,認識多執行緒模式,主要是可從中認識一些高階 API 的原理,並瞭解其適用的場景。