首先我們需要先知道Dart 是單執行緒模型的語言,也就是說程式碼執行的順序是會跟我們編寫的順序一樣,自上而下執行,但是在開發專案中,我們經常會需要做一些耗時的動作,比如說網路請求、檔案讀取等等,此時程式就會需要等待耗時的動作完成才能繼續執行,而這樣就會造成我們畫面UI 的卡頓,那麼下面我們就來看看Dart 是怎麼處理這個問題吧
isolate
是Dart
對多執行緒的實現方案,可以說是Dart
的執行緒Thread
,但和普通Thread
不同的是,isolate
是由執行緒和獨立記憶體所構成,正是由於isolate
執行緒之間的記憶體不共享,只能依靠訊息機制通訊,所以isolate
執行緒之間並不會有資源搶佔的問題,通過isolate
我們可以很好的利用多核CPU,來進行大量耗時任務的處理,其中每個isolate
都包含一個事件迴圈(Event Loop) 以及兩個事件佇列(MicroTask Queue 以及 Event Queue)
當你執行一個Flutter
或任何Dart
程式時,將建立並啟動一個預設的isolate
,可以稱之為main isolate
,Dart
會自動初始化main isolate
的兩個先進先出 (FIFO) 的佇列,即MicroTask Queue
以及 Event Queue
,然後先執行完Dart
的入口方法main()
後,事件迴圈 Event Loop
開始啟動,如果不開啟新的isolate
,所有的事件都會排在main isolate
的事件迴圈中處理
Event Loop
處理Event Queue
的流程圖:
Event Loop
為一個無限迴圈,它在MicroTask、Event Queue
中查詢是否有需要執行的事件,如果有要執行的事件,且CPU空閒時,Event Loop
會取出此事件並執行其對應需要執行的程式碼塊
Microtask Queue
可以自己向isolate
內部新增事件,事件的優先順序比Event Queue
高,但通常來源於Dart
內部,並且非常少,這是因為如果Microtask Queue
非常多的話,就會造成Event Queue
排不上隊,會阻塞任務佇列的執行 (比如使用者點選沒有反應的情況),為了保證UI 顯示順暢不卡頓,應該儘量將耗時操作放在Event Queue
中
Event Queue
負責處理I/O事件、繪製事件、手勢事件、接收其他isolate
訊息等外部事件,每次外部事件被觸發時,要執行的相應程式碼都會被新增到Event Queue
中,當MicroTask Queue
中沒有任何內容時,Event Loop
才會從Event Queue
中取出第一項來處理
( 後面會介紹的Future 也會被新增到 Event Queue
中 )
程式執行的流程圖:
Dart
的入口是main()
函式,main()
函式中的程式碼會優先執行main()
後,啟動Event Loop
,開始執行佇列中的需要被執行的任務MicroTask Queue
是否為空,MicroTask Queue
不為空的話,按照先進先出的順序去執行最先進來MicroTask Queue
的事件MicroTask
事件後,再去重複第三步,檢查MicroTask Queue
是否為空,若不為空,一樣再照順序去執行事件,直到MicroTask Queue
為空,才去執行Event Queue
Event Queue
也按照先進先出的順序去執行最先進來Event Queue
的事件,每次執行完事件後,再次返回第三步並重複動作直到兩個佇列都沒有事件要執行那為什麼單一個isolate
還是可以實現非同步呢?
這是因為我們的 App 絕大多數時間都是空閒的狀態。比如,等使用者點選、等網路請求回覆、檔案讀寫的I/O,等等,而這些等待的行為並不會阻塞我們的執行緒,這是因為這些網路請求、檔案讀寫的I/O,我們都可以非阻塞式的呼叫這些事件,使這些事件處理都不會阻塞我們單執行緒的繼續執行!
- 阻塞式呼叫: 呼叫結果返回之前,當前執行緒會被佔用著,只有在得到呼叫結果之後才會繼續執行
- 非阻塞式呼叫: 呼叫執行之後,當前執行緒不會停止執行,等待的過程中可以執行的事件,等非阻塞式呼叫的事件回調結果後,會再去執行相對應的事件 (比如網路請求,Socket 本身提供了 select 模型可以非同步查詢,而檔案 I/O,作業系統也提供了事件的回撥機制),我們也可以使用
Dart
的Future、async 以及await,來建立一個非同步事件
非同步執行的任務,會在未來某個時間點完成,成功時返回任務執行的結果 (即 Future<T>
的泛型T
物件),失敗時則返回錯誤異常
如果我們是同步的對網路做請求 (用 getNetworkData()
模擬一個網路請求):
import "dart:io";
main(List<String> args) {
print("main function start");
print(getNetworkData());
print("main function end");
//印出 main function start
//(停個三秒)
//印出 network data
//印出 main function end
}
String getNetworkData() {
sleep(Duration(seconds: 3));
return "network data";
}
而上述getNetworkData()
會阻塞住我們的main()
,只有在得到結果之後才會繼續執行,所以我們要把它修改成非同步的網路請求
使用Future
:
import "dart:io";
main(List<String> args) {
print("main function start");
print(getNetworkData());
print("main function end");
/*
印出 main function start
(沒有出現任何的阻塞現象)
印出 Instance of 'Future<String>'
印出 main function end
*/
}
Future<String> getNetworkData() {
return Future<String>(() {
sleep(Duration(seconds: 3));
return "network data";
});
}
透過Future
,我們就能夠將解決耗時事件隔離了起來,且不會再佔用住我們的執行緒了,但問題是,我們這樣只是取得一個Future
的例項,我們要如何去拿到我們要的結果呢?
答案就是通過Future
例項使用.then
取得回傳結果
import "dart:io";
main(List<String> args) {
print("main function start");
// 使用變數接收getNetworkData返回的future
var future = getNetworkData();
// 當future例項有返回結果時,會自動回撥then中傳入的函式
// 該函式會被放入到事件迴圈中,被執行
future.then((value) {
print(value);
});
print("main function end");
/*
印出 main function start
印出 main function end
(3秒後)
印出 network data
*/
}
Future<String> getNetworkData() { //Future<T>,.then 會接收返回的泛型T 物件
return Future<String>(() {
sleep(Duration(seconds: 3));
return "network data";
});
}
那如果在非同步的時候發生異常,會是什麼情況呢?
import "dart:io";
main(List<String> args) {
print("main function start");
var future = getNetworkData();
future.then((value) {
print(value);
}).catchError((error) { // 捕獲出現異常時的情況
print(error); //異常時要做的處理
});
print("main function end");
/*
印出 main function start
印出 main function end
(3秒後)
Exception: 網路請求出現錯誤
*/
}
Future<String> getNetworkData() {
return Future<String>(() {
sleep(Duration(seconds: 3));
// 模擬一個異常出現
throw Exception("網路請求出現錯誤");
});
}
Future 的鏈式呼叫
import "dart:io";
main(List<String> args) {
print("main function start");
getNetworkData().then((value1) {
print(value1);
return "content data2";
}).then((value2) {
print(value2);
return "message data3";
}).then((value3) {
print(value3);
});
print("main function end");
/*
印出 main function start
印出 main function end
(3秒後)
印出 network data1
印出 content data2
印出 message data3
*/
}
Future<String> getNetworkData() {
return Future<String>(() {
sleep(Duration(seconds: 3));
return "network data1";
});
}
Future 其他的 API
Future.value(value)
直接取得一個已完成的Future
,會直接呼叫執行.then
函式
main(List<String> args) {
print("main function start");
Future.value("test value").then((value) {
print(value);
});
print("main function end");
}
/*印出
main function start
main function end
test value
*/
//test value 比較晚印出是因為Future 的.then 會作為新的事件,加入到Event Queue,故在Event Loap輪到它時才會執行
Future.error(object)
同上,但是是獲得一個發生異常的Future
,會直接呼叫執行.catchError
函式
main(List<String> args) {
print("main function start");
Future.error(Exception("錯誤資訊")).catchError((error) {
print(error);
});
print("main function end");
/*印出
main function start
main function end
Exception: 錯誤資訊
*/
}
Future.delayed(時間, 回調函式)
延遲一定時間後,執行回調函式,執行完回調函式後會再執行then
main(List<String> args) {
print("main function start");
Future.delayed(Duration(seconds: 3), () {
print("delayed");
return "3秒後的資訊";
}).then((value) {
print(value);
});
print("main function end");
/*印出
main function start
main function end
delayed
3秒後的資訊
*/
}
Future
是非同步任務的封裝,藉助於await
與async
,我們可以通過事件迴圈實現非阻塞的同步等待
被async
修飾過的方法會將一個 Future
物件作為返回值,該方法會同步執行其中的程式法,直到第一個await
關鍵字,然後會暫停執行,等await
引用的Future
執行完成,await
之後的程式碼才會繼續執行
Future<String> getNetworkData() async {
var result = await Future.delayed(Duration(seconds: 3), () {
return "network data";
});
return "請求到的資料:" + result;
}
test() async {
var r = await getNetworkData();
print(r);
}
void main() {
print("main start");
test();
print("main end");
}
/*
印出
main start
main end
請求到的資料:network data
*/
通過dart
的async
library 下的scheduleMicrotask
來建立一個MicroTask
:
import "dart:async";
main(List<String> args) {
scheduleMicrotask(() {
print("Hello MicroTask");
});
//印出 Hello MicroTask
}
如果我們有一個任務不希望它放在Event Queue中依次排隊,那麼就可以建立一個MicroTask
我們已經知道一個isolate
有自己可以訪問的記憶體空間以及需要執行的事件迴圈,但是,如果只有一個isolate
,那麼意味著我們只能永遠利用一個執行緒,這對於多核CPU來說,是一種資源的浪費,所以在開發中,如果有非常多耗時的計算,可以自己建立isolate
,在獨立的isolate
中完成想要的計算操作
在 isolate
中,資源隔離做得非常好,每個 isolate
都有自己的 Event Loop 與 Queue,isolate
之間不共享任何資源,只能依靠訊息機制通訊,因此也就沒有資源搶佔問題
我們可以通過Isolate.spawn
建立一個isolate
import "dart:isolate";
main() {
Isolate.spawn(greet, "Hello Isolate");
}
void greet(info) {
print("新的isolate:$info");
}
isolate 之間的通訊機制,我們需要新的Isolate進行計算,並且將計算結果告知Main Isolate (也就是預設開啟的Isolate),isolate 通過傳送管道(SendPort)實現訊息通訊機制,我們可以在新增isolate 時將Main Isolate的傳送管道作為引數傳遞給它,建立的 isolate 執行完畢時,可以利用這個管道給Main Isolate傳送資料
import "dart:isolate";
main(List<String> args) async {
// 1.建立管道
ReceivePort receivePort= ReceivePort();
// 2.建立新的Isolate
Isolate isolate = await Isolate.spawn<SendPort>(foo, receivePort.sendPort);
// 3.監聽管道訊息
receivePort.listen((data) {
print('Data:$data');
// 不再使用時,我們會關閉管道
receivePort.close();
// 需要將isolate釋放
isolate?.kill(priority: Isolate.immediate);
});
}
void foo(SendPort sendPort) {
sendPort.send("Hello World");
}
但這只是單向通訊,要怎麼雙向通訊呢?
範例:
import 'dart:async';
import 'dart:isolate';
main() async {
// isolate所需的參數,必須要有SendPort,SendPort需要ReceivePort来創建
final receivePort = new ReceivePort();
// 開始創建isolate, Isolate.spawn函數是isolate.dart裡的代碼,_isolate是我們自己實現的函數
await Isolate.spawn(_isolate, receivePort.sendPort);
// 發送的第一個訊息,是它的SendPort
var sendPort = await receivePort.first;
var msg = await sendReceive(sendPort, "foo");
print('received $msg');
msg = await sendReceive(sendPort, "bar");
print('received $msg');
}
// 新isolate的入口函數
_isolate(SendPort replyTo) async {
// 實例化一個ReceivePort 以接收消息
var port = new ReceivePort();
// 把它的sendPort發送給宿主isolate,以便宿主可以給他發送訊息
replyTo.send(port.sendPort);
// 監聽消息,從port裡取出
await for (var msg in port) {
var data = msg[0];
SendPort replyTo = msg[1];
replyTo.send('回答:' + data);
if (data == "bar") port.close();
}
}
// 對某個port發送消息,並接收結果
Future sendReceive(SendPort port, msg) {
ReceivePort response = new ReceivePort();
port.send([msg, response.sendPort]);
return response.first;
}
應該什麼時候使用Futures 和 Isolate?
建議盡可能使用Future
,因為一旦事件迴圈擁有空閒時間,這些Future
就會被執行。讓使用者可以感覺事件正在被並行處理,如果確定事件需要繁重的處理以及一些時間才能完成,才考慮使用isolate
使用isolate
例子:
網路請求:解碼JSON(HttpRequest的響應)可能需要一些時間 → 可以使用compute
compute:
適合一次性的運算,不需要與isolate 雙向溝通
主要功能有:產生一個isolate,在該isolate 運行一個回調函式,並傳遞一些數據,最後回調函式返回處理結果,回調執行完後釋放isolate。 (回調函式必須是頂級函數並且不能是閉包或類中的方法 ( 靜態或非靜態) )
終於結束Dart 語言介紹的系列了,花了那麼多天,希望能把根基打穩,之後開發專案才能更清楚語法上的應用,接下來終於可以進入我們的Flutter 專案了!