本文首發于語雀https://www.yuque.com/hysteria/oemyze/cusxgq,轉載請注明出處。
在學習Java的時候,多線程是我們望而卻步的東西,但是接觸了Dart之后,發現它是單線程。但其實這個單線程的運行模型也包含非常多的內容在里面,同樣讓人不想繼續看下去。(回家種地警告)
但是作為Flutter中重要的一部分,我們必須要研究明白才能深入其整個宏觀世界,因此這個系列,將從幾部分來展開分析一下Flutter中的異步編程。
這里我分成兩大部分來分析這個事情。第一部分是Dart的異步模型,第二個是上層封裝。
異步模型分析
我們知道在一個稍微復雜點的程序運行時,總是會伴有一些網絡,IO的操作。而在GUI系統中,如果這類耗時操作被放到主線程來執行,那么用戶的操作就會無法及時響應,這個肯定是不能夠接受的。而就算在Server工程中,如果用單線程的處理請求也是該被勸退了。而多線程帶來的問題也是非常多,這就涉及到更多的,如同步鎖問題,線程池等等……所以多線程在各自的技術棧中都是非常復雜的一部分。而今天的主角Dart,偏偏就選擇了單線程。
這個對Dart開發者實在是太友好了,不用考慮太多關于多線程的問題就可以完成復雜的異步操作。但是話又說回來,如果是單線程,上面說的GUI問題豈不是就出現了么?其實不然,我們可以繼續往下看。
首先我們來考慮一個問題。多線程模型里,實現GUI交互是怎么樣的。這里以點擊按鈕請求數據更新界面舉例。
主線程點擊按鈕 -> 創建子線程進行網絡請求 -> 線程通信發送數據 -> 更新GUI
如上所示,我們說常見的Hander其實就是線程通信方式的一種。那么在單線程模型中,Dart,或者JavaScript則是借助于單線程循環模型來實現這個操作的。
先不說這個模型是啥樣子,到這里很多同學就開始有疑問了。這不卡主線程?耗時操作怎么辦的啊喂。但其實仔細想想,我們并不是每時每刻都在與界面進行交互,也并不是無時無刻在進行網絡與IO操作,這就決定了程序在大部分時間都是空閑的。既然如此,那么用戶交互,界面繪制與網絡請求就能夠被安排在一個進程中。這時候你可能又會說了,就算可以,那他在網絡請求的時候遇到用戶交互事件怎么辦?那豈不是還是不能響應。這就不得不提到操作系統中一個非常重要的概念了 -- "時間片"。也就是說,操作系統不會讓某個線程無休止的運行直到結束,而是將任務切成不同的時間片,某一時刻運行一個線程的其中一片。給人造成多線程并行執行的假象,這其實也就是“并發”的概念。這里說的是多線程,那么我們繼續往微觀角度想,如果一個線程里的n個任務單元可以如此,那豈不是就可以給人一種多個任務在一起運行的假象呢?嗯,有人比你先想到了,于是就有了協程。那么先不說Dart里面這個叫不叫做協程,但是結論就是這樣了。
運行為什么不卡頓的問題解決了,那么還有一個問題。單線程模型能夠利用好多核的能力么?這個后面會做解答。
Dart異步模型
接下來就是大家看過很多次的異步模型了,這里我從別的文章上“借鑒”了一張圖。
從圖中可以看出模型中有兩個隊列,事件隊列(Event Queue)與微任務隊列(Microtask Queue)。而這個模型的運行規則是。
- 啟動App
- 首先執行main方法里的代碼。
- main方法執行完成之后,開始遍歷執行微任務隊列,直到微任務為空。
- 繼續向下執行事件隊列,執行完一個就查詢微任務隊列是否有可以執行的微任務
- 然后兩個隊列的執行就一直按照這樣的循環方式執行下去,直到App退出
那么相比到這里大家開始疑問什么樣的叫做微任務,什么樣的又可以稱為事件?下面就解釋一下這兩種的區別,以及為什么要設計兩個隊列。
微任務
微任務在圖中是一個優先級非常高的角色,可以看到。每次都是微任務優先執行,一有微任務,不過是先來的后來的都需要無條件執行。微任務可以通過下面的方式加入。
scheduleMicrotask(() => print('This is a microtask'));
考慮到這個任務的優先級比較高,我們平時也不會用這種方法來執行異步任務。Flutter內部也只有幾處用到了這個方法(比如,手勢識別、文本輸入、滾動視圖、保存頁面效果等需要高優執行任務的場景)。
事件
事件的范圍就廣了一點,比如網絡,IO,用戶操作,繪圖,計時器等。而這個事件還有一個重要封裝,就是Future,從名字可以看出含義就是未來執行的一段代碼。
為什么單線程?
結合單線程模型和之前說的協程部分我們可以大概知道了Dart的運行規則。這個時候我們大概可以解答之前留下的疑問了,Dart的單線程模型怎么發揮CPU多核優勢呢?
下面是我個人的一點見解,如果有不同的觀點可以指出。
其實我們看JavaScript為什么用單線程模型,也就知道Dart為什么也要用了。Dart誕生是為了“Battle”JS的,但目前看來應該是失敗了。我們從網上查閱資料,就會發現這樣的段落。
JavaScript的單線程,與它的用途有關。作為瀏覽器腳本語言,JavaScript的主要用途是與用戶互動,以及操作DOM。這決定了它只能是單線程,否則會帶來很復雜的同步問題。比如,假定JavaScript同時有兩個線程,一個線程在某個DOM節點上添加內容,另一個線程刪除了這個節點,這時瀏覽器應該以哪個線程為準?
為了利用多核CPU的計算能力,HTML5提出Web Worker標準,允許JavaScript腳本創建多個線程,但是子線程完全受主線程控制,且不得操作DOM。所以,這個新標準并沒有改變JavaScript單線程的本質。
這兩段話說到了兩個事。一個是多線程容易發生并發問題,第二個是JS也嘗試利用多核CPU的能力,但是也只是閹割版的多線程。因此我們也就可以大致類比到,Dart其實也是基于此考慮的,畢竟Dart生來是想用在Web的,但是最后用在了移動端。但其實恰好所有的GUI系統都不是特別需要非常多的線程的,(相信許多Android開發者都沒怎么用過多線程的鎖之類的吧),最常見的也就是2,3個線程在做事情。但是退一萬步講,就真的是非常復雜的,要開許多個線程怎么辦?也就是說情況越極端,對CPU的利用能力與原生差異就越明顯。這個時候Dart其實也考慮過了,就是它還是運行你創建線程的。
這個“線程”叫做isolate。
isolate
isolate在這里翻譯成“隔離”,從名字就可以看出來,不同的isolate都是獨立的。這個與你說認知的Thread是有差異的。所以Dart還是保守了。難道是怕寫不出DougLea老爺子那么優雅的代碼?沒有了多線程的共享問題,也就不用寫各種同步鎖,CAS原子等機制,但隨之帶來的問題就是通信了。isolate的通信是靠著port的,這里不展開說。所以更像是 Future是線程,isolate是進程。
到這里我們可以拋下的疑問都解決了。
可以看到,單線程模型與多線程模型沒有孰好孰壞,只有在他們各自擅長的場景才能展示出自己最大的的性價比。也正是如此,我們在Android開發中用多線程的時候,也不是盲目的去new Thread,而是優先會考慮線程池。大部分情況下,適當的線程可以更好的利用CPU不會消耗很大的資源,而且也能夠得心應手的處理完所有任務。不會造成資源的浪費。
平時開發業務很少用到isolate,一方面是它通信很麻煩,另一方面我們并沒有太大的需求要用這個,但是如果真的有需要的場景,其實是不建議盲目用一堆Future的,這樣除了代碼簡單之外,沒有什么好處。
Future的分析(1)
前面說到Future其實是對事件的上層封裝,但是實際的運行過程也有不一樣的表現。為什么這么說,可以看到下面的分析。首先我們從Future這個類說起。
首先我們看到,Future是有幾個構造方法的,此外沒有在這個圖片上表現出來的是他的默認構造,下面分別來說一下這幾個構造方法。
Future(FutureOr<T> computation())
默認構造函數,此函數接受一個返回FutureOr類型的函數類型
factory Future(FutureOr<T> computation()) {
_Future<T> result = new _Future<T>();
Timer.run(() {
try {
result._complete(computation());
} catch (e, s) {
_completeWithErrorCallback(result, e, s);
}
});
return result;
}
通過這個方法創建的Future,最后會被添加到EventQueue中。而這里FutureOr這個類其實就是以這個名字來告訴你,可以返回包括Future在內的所有類型,其實并沒有相應的實現。這里由Timer.run來調度了computation參數。
Future.microtask(FutureOr<T> computation())
微任務構造。
factory Future.microtask(FutureOr<T> computation()) {
_Future<T> result = new _Future<T>();
scheduleMicrotask(() {
try {
result._complete(computation());
} catch (e, s) {
_completeWithErrorCallback(result, e, s);
}
});
return result;
}
從代碼看,不一樣的地方在于這次是使用scheduleMicrotask來調度的,我們前面說過,通過這個方法就可以創建一個微任務,因此這個computation方法將會被添加到微任務隊列中執行。
這里來一個小例子看一下誰更快被執行。
void main(){
Future(() => print("event task!"));
Future.microtask(() => print("micro task!"));
}
事實證明,micro task確實會優先被調度。
Future.sync(FutureOr<T> computation()) {}
看名字是一個同步的方法。
factory Future.sync(FutureOr<T> computation()) {
try {
var result = computation();
if (result is Future<T>) {
return result;
} else {
return new _Future<T>.value(result as dynamic);
}
} catch (error, stackTrace) {
……
}
}
這個方法是直接取了computation結果,如果結果是Future,就直接返回,否則使用value方法調度。
這里可以再做個對比。
void main(){
Future(() => print("event task!"));
Future.microtask(() => print("micro task!"));
Future.sync(() {
print("sync task");
});
}
由于sync方法的參數提前被執行,就相當于在main方法層面執行的,這個順序也與我們上面提到的線程模型完全相符。
Future.value([FutureOr<T>? value])
這個方法上面提到過了,而他的實現也比較特殊。
factory Future.value([FutureOr<T>? value]) {
return new _Future<T>.immediate(value == null ? value as T : value);
}
_Future.immediate(FutureOr<T> result) : _zone = Zone._current {
_asyncComplete(result);
}
void _asyncComplete(FutureOr<T> value) {
if (value is Future<T>) {
_chainFuture(value);
return;
}
_asyncCompleteWithValue(value as dynamic);
}
void _asyncCompleteWithValue(T value) {
_setPendingComplete();
//這里~
_zone.scheduleMicrotask(() {
_completeWithValue(value);
});
}
可以看到這個方法,如果是返回非Future類型,則最終調用了scheduleMicrotask將任務調度。這樣做也是有其中的原因的,因為非Future的value不需要執行,也就認為傳入即完成,則需要迅速執行其后的鏈式方法,需要用到微任務隊列。
與此情況類似的一種是這樣的。如果我們提前建立了一個Future,并且這個Future已經執行完成的時候,其后的then的調用則會被微任務隊列調度。
var future = Future(() => print("future"));
future.then((value) => print("future then"));
Future.delayed(Duration duration, [FutureOr<T> computation()?])
從函數名字中就可以看出來,這是一個延時執行的Future。
factory Future.delayed(Duration duration, [FutureOr<T> computation()?]) {
……
_Future<T> result = new _Future<T>();
new Timer(duration, () {
if (computation == null) {
result._complete(null as T);
} else {
try {
result._complete(computation());
} catch (e, s) {
_completeWithErrorCallback(result, e, s);
}
}
});
return result;
}
相信大家已經可以猜到,內部與默認構造函數幾乎一樣,只是利用了Timer的計時功能,時間到了之后開始調度。
Future.error(Object error, [StackTrace? stackTrace])
這個方法不太常用,是創建一個錯誤的Future,內部同value方法,也是由scheduleMicrotask進行調度的,至于這個方法存在的意思是什么,我也不太清楚了。
上面是一些基礎的構造/工廠函數,用來創建Future,但是Future也提供了一些靜態的方法,用于創建更高級的表現形式。
Future.wait
這個方法用來等待多個Future的結果,如果其中一個發生了問題,那么就直接失敗。但是這個表現由eagerError參數來控制
Future.foreach
這個方法其實就算是工具了,類似于RX里的一些工具方法,循環遍歷列表,然后每次讀取到一個數據,就調用一下回調。
Future.forEach({1,2,3}, (num){
return Future.delayed(Duration(seconds: num),(){print(num);});
});
Future.any
返回第一個Future執行完的結果,不管這個結果是正確與否。
static Future<T> any<T>(Iterable<Future<T>> futures) {}
Future.doWhile
循環執行回調操作,直到它返回false
static Future doWhile(FutureOr<bool> action())
Future的分析(2)
上面的非常大的篇幅來分析了幾個Future類里的API,我們在平時的開發中也就是利用這些API來完成的。但這些API只是用來創建Future,如果我們使用Future發起一個網絡請求,怎么能拿到請求返回的結果呢?這里就要用到我們的處理結果相關方法。而且這些方法的也會有一些配合的規律,一起來看下。
then
前面提到過then這個方法。
他就是用來處理結果的,當我們的耗時任務執行完成的時候,then就會被調用,而且多個then鏈在一起的話,還會一起調用。
//簽名
Future<R> then<R>(FutureOr<R> onValue(T value), {Function? onError});
//使用
Future(() => print("task1"))
.then((value) {
print("task2");
Future.microtask(() => print("micro task"));
})
.then((value) => print("task4"))
.then((value) => print("task5"));

這里Dart Pad這個工具沒法使用scheduleMicrotask這個方法,所有用Future.microtask代替,結果也是符合預期的。可以看到第一個then里面就調度了微任務,為什么沒有立馬執行而是執行了后續的then呢?這里then的內容是會被優先執行完的,因為此時Future已經執行完成了,需要立馬進行回調,不能進行額外的等待,所以看起來幾個then是在一起執行的。但是這種情況下,還和Future.value和 future.then這兩種情況不一樣,這個例子所有的內容都是會在Event Queue中執行的。
你可以把他們想象成,所有的then的代碼內容都在Future的耗時任務回調中,會在調度的時候一起被放到事件隊列。
但是……又有特殊情況了,如果then返回了一個Future,那么后續的then是不會被立馬執行的,而是排在這個Future之后的。

catchError
如果Future發生了異常,則需要使用catchError來捕獲。

whenComplete
類似于Java的finally,無論成功和失敗總會調用到的一個方法。
timeout
Timeout接受一個Duration類型的值,用來設置超時時間。如果Future在超時時間內完成,則就返回原Future的值,如果到達超時時間還沒有完成,就是拋出TimeoutException異常,當然,如果設置了onTimeout參數,就會以設置的返回值返回,不會產生異常。
Future<T> timeout(Duration timeLimit, {FutureOr<T> onTimeout()?});
總結
未完待續。
干就完了。