之前的兩篇文章中,我們介紹了非同步程式設計,也介紹了執行緒池的基本概念。也說了,執行緒池的實現天生也實現了非同步任務,允許直接向執行緒池中進行任務的提交與結果獲取。
但是,我們始終沒有去深入的瞭解下,非同步任務框架對於任務執行的進度是如何監控的,任務執行的結果該如何獲取。
那麼,本篇文章就來詳細地探討下非同步框架中,關於任務執行過程中的一些狀態以及執行結果反饋的相關細節。
傳統的 Future 模式
我們說過,非同步程式設計的一個好處是:
我只需要定義好任務,向 ExecutorService 中提交即可,而不用關心什麼時候,什麼執行緒在執行我們的任務。它會返回一個 Future 物件,我們通過他了解當前任務的執行細節。
Future 介面中定義了以下一些方法:
public interface Future<V> {
//取消執行當前任務
boolean cancel(boolean mayInterruptIfRunning);
//當前任務是否被取消了
boolean isCancelled();
//當前任務是否已經完成
boolean isDone();
//返回任務執行的返回結果,如果任務未完成
//將阻塞在 Future 內部佇列上等待
V get()
//新增超時限制
V get(long timeout, TimeUnit unit)
}
複製程式碼
這五個方法,每一個都很重要,為我們監控任務的執行提供有力的支援。而我們的 ThreadPoolExecutor 使用的是 FutureTask 作為 Future 的實現類。
而我們也不妨看看這個 FutureTask 內部都有些哪些成員:
state 和它可取的這些值共同描述了當前任務的執行狀態,是剛開始執行,還是正在執行中,還是正常結束,還是異常結束,還是被取消了,都由這個 state 來體現。
callable 代表當前正在執行的工作內容,這裡說一下為什麼只有 Callable 型別的任務,因為所有的 Runnable 型別任務都會被事先轉換成 Callable 型別,我覺得主要是統一和抽象實現吧。
outcome 是任務執行結束的返回值,runner 是正在執行當前任務的執行緒,waiters 是一個簡單的單連結串列,維護的是所有在任務執行結束之前嘗試呼叫 get 方法獲取執行結果的執行緒集合。當任務執行結束自當喚醒佇列中所有的執行緒。
除此之外,還有一個稍顯重要的方法,就是 run 方法,這個方法會在任務開始時由 ExecutorService 呼叫,這是一個很核心的方法,雖然方法體有點長,但是邏輯簡單,我們大體上概括下。
- 如果任務已經開始將退出方法邏輯的執行
- 排程任務執行,呼叫 call 方法
- 呼叫成功將儲存結果,異常則將儲存異常資訊
- 處理中斷
這裡需要額外去說一下,第三步中的 set 方法除了會將任務執行的返回結果設定到 FutureTask 的 outcome 欄位上,還會呼叫 finishCompletion 方法完成任務的呼叫,嘗試喚醒所有在等待任務執行結果的執行緒。
其他的方法就不去看了,也比較多,還算是簡單的,如果有所想法,也歡迎你和我探討交流。
那麼,我們也來看一個最簡單的應用示例:
我們向執行緒池提交了一個任務,這個任務的工作量不大,就是睡覺然後返回執行結果。而我們可以直接呼叫 get 方法去獲取任務執行的結果,不過 get 方法是阻塞式的,一旦任務還未執行結束,當前執行緒將丟失 CPU 進而被阻塞到 Future 的內部佇列上。
所以,推薦大家在 get 返回結果之前,先判斷下目標任務是否已經執行結束,進而避免當前執行緒的阻塞喚醒所帶來的代價。
到這裡,相信你也一定看出來了,FutureTask 實現的 Future 的弊端在 get 方法,這個方法非非同步,如果沒有成功獲取到任務的執行結果就將直接阻塞當前執行緒,以等待任務的執行完成。
但是,有一種情境,當我們向執行緒池中提交了很多工,但是不清楚各個任務的執行效率,也就是不知道誰先執行結束,如果直接 get 某個未完成的任務,將導致當前執行緒阻塞等待。
那麼我們能不能阻塞,直接獲取已經執行結束的任務 Future,而未完成的任務不允許獲取它的 Future?
使用 CompletionService
分析 CompletionService 之前,我們搬出之前分析過的一張類圖:
左半邊的類我們已經在前面的文章中都涉獵了,唯獨落下了 CompletionService 這個介面,我們當時說以後會分析它的,現在我們來看看這個介面會給我們帶來哪些能力。
首先,從類的繼承體系上來看,CompletionService 並不與我們的 Executor 產生任何直接關係,執行緒池的實現也沒有繼承該介面。
實際上來說,CompletionService 只是利用了 Executor 乃至執行緒池為自己提供任務的提交與執行能力,而自己不過額外的維護一個佇列,儲存著所有已經完成的任務的 Future,以至於我們可以直接在外部呼叫 take 方法直接獲取已完成的任務返回結果,無需阻塞。
廢話不多說,我們寫個小 demo,或許你會有更直接的體驗:
==要求:使用多執行緒計算 1-10000 之間的總和==
==思路:分段計算,最後總和相加==
實現:
相信你執行後一定和我是同樣的答案:50005000
可能很多人會有疑問,這段程式碼其實也沒什麼特別的地方啊,我使用基本的執行緒池不一樣也能實現嗎?
但是,實際上並沒有那麼簡單,因為你不能確定哪個任務完成了,哪個還沒有,所以你至少需要寫五個迴圈自旋等待。
而如果你的運氣不好,第一個任務特別慢,即便後續的任務已經結束了,主執行緒也依然由於第一個任務的結果拿不到而阻塞,耽誤了對其他已完成任務的返回結果處理。
乍一看,你可能覺得差別不大,但仔細分析了才會發現,一旦任務量增大、增多,真的是「差之毫釐,謬以千里」。
其實,原理我也可以帶大家一起來看看,並不難:
先從大家最關心的 CompletionService 實現子類內部結構開始:
這裡,至少可以看出來兩點,欄位 executor 是一個任務排程器,completionQueue 是一個阻塞佇列。
也就是說,Completion 是完全依賴外部傳入的 Executor 來實現任務的提交與執行的。而這個阻塞佇列 completionQueue 就是儲存的所有已經完成的任務 Future 物件。
除此之外,ExecutorCompletionService 還自定義了一個內部類 QueueingFuture,重寫了 FutureTask 的 done 方法。
可能大家對這個 done 沒什麼印象,但是還記得我們說過的 finishCompletion 方法嗎?
FutureTask 抽象的描述了一個任務,當執行緒啟動後將呼叫 FutureTask 內部的 run 方法執行任務的核心邏輯,並在執行的最後呼叫 finishCompletion 喚醒所有阻塞在自己佇列上等待返回結果的執行緒。
而其中 finishCompletion 方法在結束前,會呼叫一個 done 方法,這個 done 方法在 FutureTask 中是空實現,沒有任何的程式碼實現,表示並沒有什麼用。
但是我們的 QueueingFuture 充分利用這一點,重寫了 done 方法,而邏輯就是將已結束的任務新增到我們在外部維護的一個新佇列 completionQueue 中,供外部獲取呼叫。
這些就是 CompletionService 的祕密。