前言
我們知道Flutter 框架有出色的渲染和互動能力。支撐起這些複雜的能力背後,實際上是基於單執行緒模型的 Dart。那麼,與原生 Android 和 iOS 的多執行緒機制相比,單執行緒的 Dart 如何從語言設計層面和程式碼執行機制上保證 Flutter UI 的流暢性呢?
我們從下面幾個方面闡述一下:
- Dart 語言單執行緒模型和 Event Loop 處理機制
- 非同步處理和併發程式設計的原理和使用方法
- Dart 單執行緒模型下的程式碼執行本質
1. Dart單執行緒模型
dart是單執行緒執行的。怎麼理解這句話呢, 從下面幾個方面可以看到這個設計思想.
1.1 預設單一執行的執行緒
dart預設執行在Main函式存線上程,在dart中稱之為isolate,這個執行緒我們可稱之為main isolate。單執行緒任務處理的,如果不開啟新的isolate,任務預設在主isolate中處理。一旦 Dart 函式執行,它將按照在 main 函式出現的次序一個接一個地持續執行,直到退出。換而言之,Dart 函式在執行期間,無法被其他 Dart 程式碼打斷。
1.2 獨享記憶體
Android和IOS可以自由的開闢除了UI主執行緒之外的執行緒,這些執行緒和主執行緒可以共享記憶體的變數,但是, Dart中的isolate無法共享記憶體。Isolate 不能共享記憶體,他們就像是單獨的分離的 app,通過訊息進行溝通。除了顯式指定程式碼執行在別的 isolate 或者 worker 中,其他程式碼都執行在 app 的 main isolate 中。更多資訊可以訪問Use isolates or workers if necessary
1.3 質疑
(1)假如有一個任務(讀寫檔案或者網路)耗時10秒,並且加入到了事件任務佇列中,執行單這個任務的時候不就把執行緒卡主嗎?
答:檔案I/O和網路呼叫並不是在Dart層做的,而是由作業系統提供的非同步執行緒,他倆把活兒幹完之後把結果剛到佇列中,Dart程式碼只是執行一個簡單的讀動作。
(2)單執行緒模型是指的事件佇列模型,和繪製介面的執行緒是一個嗎?
答:我們所說的單執行緒指的是主Isolate。而GPU繪製指令有單獨的執行緒執行,跟主Isolate無關。事實上Flutter提供了4種task runner,有獨立的執行緒去執行專屬的任務:參見:深入理解Flutter引擎執行緒模式
- Platform Task Runner:處理來自平臺(Android/iOS)的訊息
- UI Task Runner:執行渲染邏輯、處理native plugin的訊息、timer、microtask、非同步I/O操作處理等
- GPU Task Runner:執行GPU指令
- IO Task Runner:執行I/O任務
2. Event Loop 機制
如圖所示,dart也存在事件佇列和事件迴圈。每個isolate也包含一個事件迴圈,區別是他有兩個事件佇列,event loop事件迴圈,以及event queue和microtask queue事件佇列,event和microtask佇列有點類似iOS的source0和source1。
- event queue:負責處理I/O事件、繪製事件、手勢事件、接收其他isolate訊息等外部事件。
- microtask queue:可以自己向isolate內部新增事件,事件的優先順序比event queue高。
- 先檢查
MicroTask
佇列是否為空,非空則先執行MicroTask
佇列中的MicroTask - 一個
MicroTask
執行完後,檢查有沒有下一個MicroTask
,直到MicroTask
佇列為空,才去執行Event
佇列 - 在
Evnet
佇列取出一個事件處理完後,再次返回第一步,去檢查MicroTask
佇列是否為空
我們可以看出,將任務加入到MicroTask
中可以被儘快執行,但也需要注意,當事件迴圈在處理MicroTask
佇列時,會阻塞event佇列的事件執行,這樣就會導致渲染、手勢響應等event事件響應延時。為了保證渲染和手勢響應,應該儘量將耗時操作放在event佇列中。
我們通常很少會直接用到微任務佇列,就連 Flutter 內部,也只有 7 處用到了而已(比如,手勢識別、文字輸入、滾動檢視、儲存頁面效果等需要高優執行任務的場景)。
簡單總結為一二一模型:1個事件迴圈和2個佇列的單執行緒執行模型。
3. 非同步任務排程
為什麼單執行緒也可以非同步?這裡有一個大前提,那就是我們的 App 絕大多數時間都在等待。比如,等使用者點選、等網路請求返回、等檔案 IO 結果,等等。而這些等待行為並不是阻塞的。比如說,網路請求,Socket 本身提供了 select 模型可以非同步查詢;而檔案 IO,作業系統也提供了基於事件的回撥機制。所以,基於這些特點,單執行緒模型可以在等待的過程中做別的事情,等真正需要響應結果了,再去做對應的處理。因為等待過程並不是阻塞的,所以給我們的感覺就像是同時在做多件事情一樣。但其實始終只有一個執行緒在處理你的事情。
非同步任務我們用的最多的還是優先順序更低的 Event Queue。比如,I/O、繪製、定時器這些非同步事件,都是通過事件佇列驅動主執行緒執行的。
3.1 用Future發起非同步任務
Dart 為 Event Queue 的任務建立提供了一層封裝,叫作 Future。Future 還提供了鏈式呼叫的能力,可以在非同步任務執行完畢後依次執行鏈路上的其他函式體。
new Future((){
// doing something
});複製程式碼
微任務是由 scheduleMicroTask 建立的。如下所示,這段程式碼會在下一個事件迴圈中輸出一段字串:
scheduleMicrotask(() => print('This is a microtask'));複製程式碼
鏈式呼叫:
Future(() => print('Running in Future 1'));//下一個事件迴圈輸出字串
Future(() => print(‘Running in Future 2'))
.then((_) => print('and then 1'))
.then((_) => print('and then 2’));//上一個事件迴圈結束後,連續輸出三段字串複製程式碼
Dart 會將非同步任務的函式執行體放入事件佇列,然後立即返回,後續的程式碼繼續同步執行。而當同步執行的程式碼執行完畢後,事件佇列會按照加入事件佇列的順序(即宣告順序),依次取出事件,最後同步執行 Future 的函式體及後續的 then。這意味著,then 與 Future 函式體共用一個事件迴圈。而如果 Future 有多個 then,它們也會按照鏈式呼叫的先後順序同步執行,同樣也會共用一個事件迴圈。
如果 Future 執行體已經執行完畢了,但你又拿著這個 Future 的引用,往裡面加了一個 then 方法體,這時 Dart 會如何處理呢?面對這種情況,Dart 會將後續加入的 then 方法體放入微任務佇列,儘快執行。
//f1比f2先執行
Future(() => print('f1'));
Future(() => print('f2'));
//f3執行後會立刻同步執行then 3
Future(() => print('f3')).then((_) => print('then 3'));
//then 4會加入微任務佇列,儘快執行
Future(() => null).then((_) => print('then 4'));
結果: f1 f2 f3 then 3 then 4複製程式碼
4. 非同步函式
Future 是非同步任務的封裝,藉助於 await 與 async,我們可以通過事件迴圈實現非阻塞的同步等待。Dart 中的 await 並不是阻塞等待,而是非同步等待。Dart 會將呼叫體的函式也視作非同步函式,將等待語句的上下文放入 Event Queue 中,一旦有了結果,Event Loop 就會把它從 Event Queue 中取出,等待程式碼繼續執行。
將async
關鍵字作為方法宣告的字尾時,具有如下意義
- 被修飾的方法會將一個
Future
物件作為返回值 - 該方法會同步執行其中的方法的程式碼直到第一個 await 關鍵字,然後它暫停該方法其他部分的執行;
- 一旦由 await 關鍵字引用的 Future 任務執行完成,await的下一行程式碼將立即執行。
// 匯入io庫,呼叫sleep函式
import 'dart:io';
// 模擬耗時操作,呼叫sleep函式睡眠2秒
doTask() async{
await sleep(const Duration(seconds:2));
return "Ok";
}
// 定義一個函式用於包裝
test() async {
var r = await doTask();
print(r);
}
void main(){
print("main start");
test();
print("main end");
}
結果:
main start
main end
Ok複製程式碼
我們先來看下這段程式碼。第二行的 then 執行體 f2 是一個 Future,為了等它完成再進行下一步操作,我們使用了 await,期望列印結果為 f1、f2、f3、f4:
Future(()=>print('f1'))
.then((_)async=>awaitFuture(()=>print('f2')))
.then((_)=>print('f3'));
Future(()=>print('f4'));複製程式碼
實際上,當你執行這段程式碼時就會發現,列印出來的結果其實是 f1、f4、f2、f3!
- 分析一下這段程式碼的執行順序:
- 按照任務的宣告順序,f1 和 f4 被先後加入事件佇列。
- f1 被取出並列印;
- 然後到了 then。then 的執行體是個 future f2,於是放入 Event Queue。
- 然後把 await 也放到 Event Queue 裡。這個時候要注意了,Event Queue 裡面還有一個 f4,我們的 await 並不能阻塞 f4 的執行。因此,Event Loop 先取出 f4,列印 f4;
- 然後才能取出並列印 f2,最後把等待的 await 取出,開始執行後面的 f3。
由於 await 是採用事件佇列的機制實現等待行為的,所以比它先在事件佇列中的 f4 並不會被它阻塞。
5. Isolate
Dart 也提供了多執行緒機制,即 Isolate(這個單詞的中文意思是隔離)。在 Isolate 中,資源隔離做得非常好,每個 Isolate 都有自己的 Event Loop 與 Queue,Isolate 之間不共享任何資源,只能依靠訊息機制通訊,因此也就沒有資源搶佔問題。如下所示,我們宣告瞭一個 Isolate 的入口函式,然後在 main 函式中啟動它,並傳入了一個字串引數:
doSth(msg) => print(msg);
main() {
Isolate.spawn(doSth, "Hi");
...
}複製程式碼
那麼如何利用訊息機制進行通訊呢,下面引用了一篇文章的講解,圖畫的很好。
整個訊息通訊過程如上圖所示,兩個Isolate是通過兩對Port物件通訊,一對Port分別由用於接收訊息的ReceivePort
物件,和用於傳送訊息的SendPort
物件構成。其中SendPort
物件不用單獨建立,它已經包含在ReceivePort
物件之中。需要注意,一對Port物件只能單向發訊息,這就如同一根自來水管,ReceivePort
和SendPort
分別位於水管的兩頭,水流只能從SendPort
這頭流向ReceivePort
這頭。因此,兩個Isolate
之間的訊息通訊肯定是需要兩根這樣的水管的,這就需要兩對Port物件。
6. 引用文章
(3)Dart asynchronous programming: Isolates and event loops