文章主題是「單線程模型下如何保證 UI 的流暢性」。該話題針對的是 Flutter 性能原理展開的,可是 dart 語言就是 js 的延伸,不少概念和機制都是同樣的。具體不細聊。此外 js 也是單線程模型,在界面展現和 IO 等方面和 dart 相似。因此結合對比講一下,幫助梳理和類比,更加容易掌握本文的主題,和知識的橫向拓展。css
先從前端角度出發,分析下 event loop 和事件隊列模型。再從 Flutter 層出發聊聊 dart 側的事件隊列和同步異步任務之間的關係。前端
假設有幾個任務:c++
在單線程中執行,代碼可能以下:瀏覽器
//c void mainThread () { string name = "姓名:" + "杭城小劉"; string birthday = "年齡:" + "1995" + "02" + "20" int age = 2021 - 1995 + 1; printf("我的信息爲:%s, %s, 大小:%d", name.c_str(), birthday.c_str(), age); }
線程開始執行任務,按照需求,單線程依次執行每一個任務,執行完畢後線程立刻退出。安全
問題1 介紹的線程模型太簡單太理想了,不可能從一開始就 n 個任務就肯定了,大多數狀況下,會接收到新的 m 個任務。那麼 section1 中的設計就沒法知足該需求。網絡
要在線程運行的過程當中,可以接受並執行新的任務,就須要有一個事件循環機制。最基礎的事件循環能夠想到用一個循環來實現。數據結構
// c++ int getInput() { int input = 0; cout<< "請輸入一個數"; cin>>input; return input; } void mainThread () { while(true) { int input1 = getInput(); int input2 = getInput(); int sum = input1 + input2; print("兩數之和爲:%d", sum); } }
相較於初版線程設計,這一版作了如下改進:多線程
真實環境中的線程模塊遠遠沒有這麼簡單。好比瀏覽器環境下,線程可能正在繪製,可能會接收到1個來自用戶鼠標點擊的事件,1個來自網絡加載 css 資源完成的事件等等。第二版線程模型雖然引入了事件循環機制,能夠接受新的事件任務,可是發現沒?這些任務之來自線程內部,該設計是沒法接受來自其餘線程的任務的。併發
從上圖能夠看出,渲染主線程會頻繁接收到來自於 IO 線程的一些事件任務,當接受到的資源加載完成後的消息,則渲染線程會開始 DOM 解析;當接收到來自鼠標點擊的消息,渲染主線程則會執行綁定好的鼠標點擊事件腳本(js)來處理事件。框架
須要一個合理的數據結構,來存放並獲取其餘線程發送的消息?
消息隊列這個詞你們都聽過,在 GUI 系統中,事件隊列是一個通用解決方案。
消息隊列(事件隊列)是一種合理的數據結構。要執行的任務添加到隊列的尾部,須要執行的任務,從隊列的頭部取出。
有了消息隊列以後,線程模型獲得了升級。以下:
能夠看出改造分爲3個步驟:
僞代碼。構造隊列接口部分
class TaskQueue { public: Task fetchTask (); // 從隊列頭部取出1個任務 void addTask (Task task); // 將任務插入到隊列尾部 }
改造主線程
TaskQueue taskQueue; void processTask (); void mainThread () { while (true) { Task task = taskQueue.fetchTask(); processTask(task); } }
IO 線程
void handleIOTask () { Task clickTask; taskQueue.addTask(clickTask); }
Tips: 事件隊列是存在多線程訪問的狀況,因此須要加鎖。
瀏覽器環境中, 渲染進程常常接收到來自其餘進程的任務,IO 線程專門用來接收來自其餘進程傳遞來的消息。IPC 專門處理跨進程間的通訊。
消息隊列中有不少消息類型。內部消息:如鼠標滾動、點擊、移動、宏任務、微任務、文件讀寫、定時器等等。
消息隊列中還存在大量的與頁面相關的事件。如 JS 執行、DOM 解析、樣式計算、佈局計算、CSS 動畫等等。
上述事件都是在渲染主線程中執行的,所以編碼時需注意,儘可能減少這些事件所佔用的時長。
Chrome 設計上,肯定要退出當前頁面時,頁面主線程會設置一個退出標誌的變量,每次執行完1個任務時,判斷該標誌。若是設置了,則中斷任務,退出線程
事件隊列的特色是先進先出,後進後出。那後進的任務也許會被前面的任務由於執行時間過長而阻塞,等待前面的任務執行完畢才能夠執行後面的任務。這樣存在2個問題。
如何處理高優先級的任務
假如要監控 DOM 節點的變化狀況(插入、刪除、修改 innerHTML),而後觸發對應的邏輯。最基礎的作法就是設計一套監聽接口,當 DOM 變化時,渲染引擎同步調用這些接口。不過這樣子存在很大的問題,就是 DOM 變化會很頻繁。若是每次 DOM 變化都觸發對應的 JS 接口,則該任務執行會很長,致使執行效率的下降
若是將這些 DOM 變化作爲異步消息,假如消息隊列中。可能會存在由於前面的任務在執行致使當前的 DOM 消息不會被執行的問題,也就是影響了監控的實時性。
如何權衡效率和實時性?微任務 就是解決該類問題的。
一般,咱們把消息隊列中的任務成爲宏任務,每一個宏任務中都包含一個微任務隊列,在執行宏任務的過程當中,假如 DOM 有變化,則該變化會被添加到該宏任務的微任務隊列中去,這樣子效率問題得以解決。
當宏任務中的主要功能執行完畢歐,渲染引擎會執行微任務隊列中的微任務。所以實時性問題得以解決
如何解決單個任務執行時間過長的問題
能夠看出,假如 JS 計算超時致使動畫 paint 超時,會形成卡頓。瀏覽器爲避免該問題,採用 callback 回調的設計來規避,也就是讓 JS 任務延後執行。
Dart 是單線程的,也就是代碼會有序執行。此外 Dart 做爲 Flutter 這一 GUI 框架的開發語言,必然支持異步。
一個 Flutter 應用包含一個或多個 isolate,默認方法的執行都是在 main isolate 中;一個 isolate 包含1個 Event loop 和1個 Task queue。其中,Task queue 包含1個 Event queue 事件隊列和1個 MicroTask queue 微任務隊列。以下:
爲何須要異步?由於大多數場景下 應用都並非一直在作運算。好比一邊等待用戶的輸入,輸入後再去參與運算。這就是一個 IO 的場景。因此單線程能夠再等待的時候作其餘事情,而當真正須要處理運算的時候,再去處理。所以雖是單線程,可是給咱們的感覺是同事在作不少事情(空閒的時候去作其餘事情)
某個任務涉及 IO 或者異步,則主線程會先去作其餘須要運算的事情,這個動做是靠 event loop 驅動的。和 JS 同樣,dart 中存儲事件任務的角色是事件隊列 event queue。
Event queue 負責存儲須要執行的任務事件,好比 DB 的讀取。
Dart 中存在2個隊列,一個微任務隊列(Microtask Queue)、一個事件隊列(Event Queue)。
Event loop 不斷的輪詢,先判斷微任務隊列是否爲空,從隊列頭部取出須要執行的任務。若是微任務隊列爲空,則判斷事件隊列是否爲空,不爲空則從頭部取出事件(好比鍵盤、IO、網絡事件等),而後在主線程執行其回調函數,以下:
微任務,即在一個很短的時間內就會完成的異步任務。微任務在事件循環中優先級最高,只要微任務隊列不爲空,事件循環就不斷執行微任務,後續的事件隊列中的任務持續等待。微任務隊列可由 scheduleMicroTask
建立。
一般狀況,微任務的使用場景比較少。Flutter 內部也在諸如手勢識別、文本輸入、滾動視圖、保存頁面效果等須要高優執行任務的場景用到了微任務。
因此,通常需求下,異步任務咱們使用優先級較低的 Event Queue。好比 IO、繪製、定時器等,都是經過事件隊列驅動主線程來執行的。
Dart 爲 Event Queue 的任務提供了一層封裝,叫作 Future。把一個函數體放入 Future 中,就完成了同步任務到異步任務的包裝(相似於 iOS 中經過 GCD 將一個任務以同步、異步提交給某個隊列)。Future 具有鏈式調用的能力,能夠在異步執行完畢後執行其餘任務(函數)。
看一段具體代碼:
void main() { print('normal task 1'); Future(() => print('Task1 Future 1')); print('normal task 2'); Future(() => print('Task1 Future 2')) .then((value) => print("subTask 1")) .then((value) => print("subTask 2")); } // lbp@MBP ~/Desktop dart index.dart normal task 1 normal task 2 Task1 Future 1 Task1 Future 2 subTask 1 subTask 2
main 方法內,先添加了1個普通同步任務,而後以 Future 的形式添加了1個異步任務,Dart 會將異步任務加入到事件隊列中,而後理解返回。後續代碼繼續以同步任務的方式執行。而後再添加了1個普通同步任務。而後再以 Future 的方式添加了1個異步任務,異步任務被加入到事件隊列中。此時,事件隊列中存在2個異步任務,Dart 在事件隊列頭部取出1個任務以同步的方式執行,所有執行(先進先出)完畢後再執行後續的 then。
Future 與 then 公用1個事件循環。若是存在多個 then,則按照順序執行。
例2:
void main() { Future(() => print('Task1 Future 1')); Future(() => print('Task1 Future 2')); Future(() => print('Task1 Future 3')) .then((_) => print('subTask 1 in Future 3')); Future(() => null).then((_) => print('subTask 1 in empty Future')); } lbp@MBP ~/Desktop dart index.dart Task1 Future 1 Task1 Future 2 Task1 Future 3 subTask 1 in Future 3 subTask 1 in empty Future
main 方法內,Task 1 添加到 Future 1中,被 Dart 添加到 Event Queue 中。Task 1 添加到 Future 2中,被 Dart 添加到 Event Queue 中。Task 1 添加到 Future 3中,被 Dart 添加到 Event Queue 中,subTask 1 和 Task 1 共用 Event Queue。Future 4中任務爲空,因此 then 裏的代碼會被加入到 Microtask Queue,以便下一輪事件循環中被執行。
綜合例子
void main() { Future(() => print('Task1 Future 1')); Future fx = Future(() => null); Future(() => print("Task1 Future 3")).then((value) { print("subTask 1 Future 3"); scheduleMicrotask(() => print("Microtask 1")); }).then((value) => print("subTask 3 Future 3")); Future(() => print("Task1 Future 4")) .then((value) => Future(() => print("sub subTask 1 Future 4"))) .then((value) => print("sub subTask 2 Future 4")); Future(() => print("Task1 Future 5")); fx.then((value) => print("Task1 Future 2")); scheduleMicrotask(() => print("Microtask 2")); print("normal Task"); } lbp@MBP ~/Desktop dart index.dart normal Task Microtask 2 Task1 Future 1 Task1 Future 2 Task1 Future 3 subTask 1 Future 3 subTask 3 Future 3 Microtask 1 Task1 Future 4 Task1 Future 5 sub subTask 1 Future 4 sub subTask 2 Future 4
解釋:
異步函數的結果在未來某個時刻才返回,因此須要返回一個 Future 對象,供調用者使用。調用者根據需求,判斷是在 Future 對象上註冊一個 then 等 Future 執行體結束後再進行異步處理,仍是同步等到 Future 執行結束。Future 對象若是須要同步等待,則須要在調用處添加 await,且 Future 所在的函數須要使用 async 關鍵字。
await 並非同步等待,而是異步等待。Event Loop 會將調用體所在的函數也看成異步函數,將等待語句的上下文總體添加到 Event Queue 中,一旦返回,Event Loop 會在 Event Queue 中取出上下文代碼,等待的代碼繼續執行。
await 阻塞的是當前上下文的後續代碼執行,並不能阻塞其調用棧上層的後續代碼執行
void main() { Future(() => print('Task1 Future 1')) .then((_) async => await Future(() => print("subTask 1 Future 2"))) .then((_) => print("subTask 2 Future 2")); Future(() => print('Task1 Future 2')); } lbp@MBP ~/Desktop dart index.dart Task1 Future 1 Task1 Future 2 subTask 1 Future 2 subTask 2 Future 2
解析:
Future(() => print("subTask 1 Future 2"))
被添加到 Event Queue 中,所在的 await 函數也被添加到了 Event Queue 中。第二個 then 也被添加到 Event Queue 中Dart 爲了利用多核 CPU,將 CPU 層面的密集型計算進行了隔離設計,提供了多線程機制,即 Isolate。每一個 Isolate 資源隔離,都有本身的 Event Loop 和 Event Queue、Microtask Queue。Isolate 之間的資源共享經過消息機制通訊(和進程同樣)
使用很簡單,建立時須要傳遞一個參數。
void coding(language) { print("hello " + language); } void main() { Isolate.spawn(coding, "Dart"); } lbp@MBP ~/Desktop dart index.dart hello Dart
大多數狀況下,不只僅須要併發執行。可能還須要某個 Isolate 運算結束後將結果告訴主 Isolate。能夠經過 Isolate 的管道(SendPort)實現消息通訊。能夠在主 Isolate 中將管道做爲參數傳遞給子 Isolate,當子 Isolate 運算結束後將結果利用這個管道傳遞給主 Isolate
void coding(SendPort port) { const sum = 1 + 2; // 給調用方發送結果 port.send(sum); } void main() { testIsolate(); } testIsolate() async { ReceivePort receivePort = ReceivePort(); // 建立管道 Isolate isolate = await Isolate.spawn(coding, receivePort.sendPort); // 建立 Isolate,並傳遞發送管道做爲參數 // 監聽消息 receivePort.listen((message) { print("data: $message"); receivePort.close(); isolate?.kill(priority: Isolate.immediate); isolate = null; }); } lbp@MBP ~/Desktop dart index.dart data: 3
此外 Flutter 中提供了執行併發計算任務的快捷方式-compute 函數。其內部對 Isolate 的建立和雙向通訊進行了封裝。
實際上,業務開發中使用 compute 的場景不多,好比 JSON 的編解碼能夠用 compute。
計算階乘:
int testCompute() async { return await compute(syncCalcuateFactorial, 100); } int syncCalcuateFactorial(upperBounds) => upperBounds < 2 ? upperBounds : upperBounds * syncCalcuateFactorial(upperBounds - 1);
總結: