Two-phase Termination

January 12, 2022

在執行緒正在忙著處理事情時,如果你想停止執行緒,該怎麼做呢?例如,〈Worker Thread〉中的 Worker,執行一個無窮的任務迴圈,若不再需要某個 Worker 實例了,你要怎麼停止任務迴圈呢?

兩階段終止

如果執行緒處於正在運作中,會有中途停止執行緒的需求,那麼你就應該考量執行緒真正結束前,必須滿足哪些條件。

例如,〈Worker Thread〉的 Worker,是處於任務迴圈,當 Worker 取得任務且執行中,這時若收到了中止 Worker 的需求,就直接透過 Threadstop,強制中斷執行緒的話,那麼 Worker 目前的任務可能處於不完整的狀態。

你也許不希望有這類不完整的狀態,因此決定執行緒不該馬上終止,若 Worker 執行任務中,應該讓它完成,在下一次任務迴圈開始前終止迴圈:

class Worker extends Thread {
    private boolean isWorkable = true;
    private BlockingQueue<Runnable> runnables;
    
    Worker(BlockingQueue<Runnable> runnables) {
        this.runnables = runnables;
    }
    
    public void run() {
        while(isWorkable) {
            try {
                runnables.take().run();
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        }
    }
    
    void terminate() {
        isWorkable = false;
    }
}

isWorkable 決定了是否執行 while 迴圈本體,呼叫 terminate 前是運作任務的階段,呼叫 terminate 後是善後任務的階段,就這邊的範例來說,就是讓 Worker 處理中的任務執行完畢。

interrupt?

執行緒可能在任務處理過程進入等待狀態,如果想在送出停止需求後,讓進入等待狀態的執行緒醒來,就 Java 而言,可以執行 Threadinterrupt 方法,執行緒會離開等待狀態,並從等待處拋出 InterruptedException

你得考量 InterruptedException 要如何處理,就 Worker 來說,可能會是在取得任務的過程,因為沒有任務而等待,因此處理上相對而言比較單純:

class Worker extends Thread {
    private boolean isWorkable = true;
    private BlockingQueue<Runnable> runnables;
    
    Worker(BlockingQueue<Runnable> runnables) {
        this.runnables = runnables;
    }
    
    private Optional<Runnable> task() {
        try {
            return Optional.of(runnables.take());
        } catch (InterruptedException e) {
            ...logging
        }
        return Optional.empty();
    }
    
    public void run() {
        while(isWorkable) {
            Optional<Runnable> t = task();
            if(t.isPresent()) {
                t.get().run();
            }
        }
    }
    
    void terminate() {
        isWorkable = false;
        terminate();
    }
}

也就是因為等待任務期間發生 InterruptedException 的話,或許下個日誌,然後傳回 Optional.empty(),至於任務迴圈中,若沒有任務的話就不執行。

至於 Runnable 本身封裝的任務中,若會有某流程導致執行緒進入等待狀態,InterruptedException 會在該處拋出,那麼該任務要自己負責處理 InterruptedException,封裝任務時,若捕捉到 InterruptedException,必須思考執行緒是因哪些條件被迫中斷,才會離開等待狀態,這時要思考該做哪些收尾動作,像是清除執行緒使用的資源之類,各種情境下的處理方式,絕對不要將之私吞,什麼都不處理。