Dart基礎之Isolate
背景
在其他語言中為了高效利用多核CPU,通常使用多執行緒並行來實現併發執行程式碼,通過共享資料來保證多執行緒之間的協同,但這種模式衍生出了很多問題,開闢執行緒帶來資源消耗,資料共享代理死鎖問題。
不論是APP還是Web端,CPU大多數時間是處於空閒狀態的,一般不需要密集和高併發的處理。Dart作為面向前端開發設計的語言,在併發設計上沒有采用多執行緒方案,而是使用了Isolate(隔離區)這種單執行緒模型來解決併發任務對於多執行緒的依賴。
Isolate組成
每個Isolate由以下幾部分組成:
(1)Stack用於存放函式呼叫上下文和呼叫鏈路資訊。
(2)Heap用於存放物件,堆記憶體回收管理和java類似。
(3)用於存放非同步回撥的Queue,分為微事件佇列(MicroTaskQueue)和微任務佇列(EventQueue)。
(4)以及一個用於處理非同步回撥的EventLoop。
Isolate執行程式碼
在dart中編寫的程式碼分為兩種型別:
同步程式碼:即正常編寫的程式碼。
非同步程式碼:一些返回型別為Future和Stream的函式。
由於Isolate是一種單執行緒模型,程式碼執行時碰到非同步程式碼會將其丟到Queue中,只按順序執行同步程式碼。等同步程式碼都執行完成後才會按順序執行非同步任務。
下面寫demo驗證下這個結論,下圖的Future.delayed就是非同步程式碼。
Future printc(){
Future.delayed(new Duration(seconds: 2),(){
print("a");// 非同步程式碼的的回撥會在所有同步程式碼執行完畢後才開始執行
});
Future.delayed(new Duration(seconds: 1),(){
print("b");// 非同步程式碼的的回撥會在所有同步程式碼執行完畢後才開始執行
});
print("c");
var start = DateTime.now();
for(int i=0;i<100000;i++){
for(int j=0;j<100000;j++){
i*j+j*i-j*i;
}
}
var end = DateTime.now();
print("同步耗時任務:${end.second-start.second}秒");
print("d");
}
複製程式碼
執行結果為:
c
同步耗時任務:11秒
d
b
a
複製程式碼
解釋下結果,列印a和b的非同步任務會插入到Queue中,其回撥目前也不會執行。執行同步程式碼,所以首先列印的是c,之後等同步耗時任務執行完成後再列印d。同步程式碼執行完成後EventLoop會從Queue中提取非同步程式碼執行,雖然非同步任務a在前面,但其延時時間長,所以先列印了b。
同步等待:在呼叫非同步程式碼時(比如Future)使用await關鍵字,在方法宣告處使用async關鍵字。
如果就是想先執行非同步任務再執行同步程式碼則可以使用await
來執行非同步任務並阻塞同步程式碼。非同步任務執行完畢後會繼續執行同步程式碼。
Future printc() async{
await Future.delayed(new Duration(seconds: 2),(){
print("a");// 非同步程式碼的的回撥會在所有同步程式碼執行完畢後才開始執行
});
await Future.delayed(new Duration(seconds: 1),(){
print("b");// 非同步程式碼的的回撥會在所有同步程式碼執行完畢後才開始執行
});
print("c");
var start = DateTime.now();
for(int i=0;i<100000;i++){
for(int j=0;j<10000;j++){
i*j+j*i-j*i;
}
}
var end = DateTime.now();
print("同步耗時任務:${end.difference(start).inSeconds}秒");
print("d");
}
複製程式碼
在兩個非同步任務中新增了await修飾符,則會像同步程式碼一樣按照順序進行執行。
a
b
c
同步耗時任務:1秒
d
複製程式碼
EventLoop
非同步任務會丟到EventQueue由EventLoop來執行。會不斷的從時間佇列中獲取非同步任務並執行。
下圖展示了EventLoop從佇列中獲取任務並執行的過程。
圖中的MicrotaskQueue是微任務佇列,優先順序最高,每次迴圈都會先檢查微任務佇列,有微任務則優先執行微任務,直到所有微任務都做完後才去執行EventQueue。
Microtask:微任務一般用於執行很短的非同步操作,任務量不能太多。 生成微任務的方式有
// 方式一
scheduleMicrotask((){
print("我是一條微任務!");
});
// 方式二
Future.microtask(() => print("我是另一條微任務!"));
複製程式碼
Event:事件,主要來自於Future,已經IO、手勢、繪製、計時器和與Isloate通訊的message等。
此處需要注意Future使用的特殊場景。
Future.delayed(new Duration(seconds: 2),(){
print("a");// 非同步程式碼的的回撥會在所有同步程式碼執行完畢後才開始執行
});
複製程式碼
Future.delayed 會在延遲結束後才把非同步回撥新增到EventQueue尾部,並不是立即執行,不能保證執行時間。
Future(()=>print("zzz"))
.then((value) => print("xxx"))
.then((value) => print("yyy"))
.then((value) => print("www"));
複製程式碼
Future.then 會對非同步事件補充回撥,then不會向EventQueue中新增事件,而是在前面的Future執行完成後立即執行。可以保證多個then內部task的執行順序。
Future aaa = Future(()=>print("aaa"));
// 此時aaa已經處於完成狀態
aaa.then((value) => print("bbb"));
// Future(()=>null)生成的就是一個已經完成的Future
Future(()=>null).then((value) => print("ccc"));
複製程式碼
針對已經完成的Future,呼叫then時並不會立即執行,而是將then中的回撥新增到Microtask佇列中。
案例分析:
void dartLoopTest() {
Future x0 = Future(() => null);
Future x = Future(() => print('1'));
Future(() => print('2'));
scheduleMicrotask(() => print('3'));
x.then((value) {
print('4');
Future(() => print('5'));
}).then((value) => print('6'));
print('7');
x0.then((zvalue) {
print('8');
scheduleMicrotask(() {
print('9');
});
}).then((value) => print('10'));
}
複製程式碼
輸出結果為:
7
3
8
10
9
1
4
6
2
5
複製程式碼
-
最先執行同步程式碼
print('7');
-
其次判斷MicroTask佇列,執行
scheduleMicrotask(() => print('3'));
-
當前MicroTask佇列為空,從Event佇列中獲取事件,按照程式碼順序執行
Future x0 = Future(() => null); 複製程式碼
此時沒有輸出,但x0是完成狀態的Future,第一個then的回撥會加入到MicroTask佇列。
x0.then((zvalue) { print('8'); scheduleMicrotask(() { print('9'); }); }).then((value) => print('10')); 複製程式碼
然後執行微任務,呼叫
print('8');
,執行scheduleMicrotask((){print('9');});
向微任務佇列新增print('9');
的任務。由於當前微任務沒有執行完,所以會先呼叫print('10')
。 -
按順序執行
Future x = Future(() => print('1'));
,之後x 也處於完成狀態,then回撥會新增到微任務佇列中執行。在微任務中先列印4,再向事件佇列新增列印5的事件,最後列印6。x.then((value) { print('4'); Future(() => print('5')); }).then((value) => print('6')); 複製程式碼
-
此時事件佇列裡還有列印2和列印5的事件,按順序執行輸出最後結果。
void testThenAwait(){
Future(()=>print("AAA"))
.then((value) async => await Future(()=>print("BBB")))
.then((value) => print("CCC"));
Future(()=>print("DDD"));
}
複製程式碼
當我們在then中使用await時會阻塞當前then向下呼叫。
輸出結果:
AAA
DDD
BBB
CCC
複製程式碼
輸出分析:
- 首先這個方法中有兩個Future,按順序新增到Event佇列中。
- 執行第一個Future,列印AAA,
- 接著同步執行第一個then,向Event佇列中新增第三Future
Future(()=>print("BBB")
由於有同步等待,必須等Future(()=>print("BBB")
執行完畢後才會繼續向下執行。 - 執行Event佇列中排在前面的非同步事件
Future(()=>print("DDD"));
,列印DDD - 執行Event佇列中剩餘的非同步事件
Future(()=>print("BBB")
,列印BBB - 接著執行then,列印CCC
建立Isolate
基本方法
Flutter應用中的程式碼預設都跑在root isloate 中,儘管是單執行緒但已經足夠處理各類非同步任務。
當有計算密集型的耗時任務時,就需要建立新的Isolate來進行耗時計算來避免阻塞root isloate。由於不同Isolate之間記憶體隔離,要通訊就得通過 ReceivePort與SendPort 來實現。
使用Isolate.spawn來建立新的Isolate。看下函式簽名
external static Future<Isolate> spawn<T>(
void entryPoint(T message), T message,
{bool paused = false,
bool errorsAreFatal = true,
SendPort? onExit,
SendPort? onError,
@Since("2.3") String? debugName});
複製程式碼
external
修飾說明這個方法在不同的平臺有不同的實現,有點類似於java的native方法。
Future<Isolate>
說明是非同步方法,要想同步程式碼中拿到建立好的Isolate,呼叫時得新增同步等待await。
entryPoint
定義了所能接受的計算任務的函式簽名(空返回值,有且只有一個入參),必須是頂層函式或者靜態方法。
T message
是在新Isolate中執行計算任務所需的引數,型別必須和entryPoint所能接受的型別一致。
所以建立新的Isolate最主要的就以下兩步:
-
使用頂層函式或靜態方法定義計算任務
// 耗時計算部分 int fibonacci(int n) { return n < 2 ? n : fibonacci(n - 2) + fibonacci(n - 1); } // 1. 計算任務 void task1(int start) { DateTime startTime = DateTime.now(); int result = fibonacci(start); DateTime endTime = DateTime.now(); print("計算耗時:${endTime.difference(startTime)} 結果:${result.toString()}"); } void main() { task1(50); } // 輸出:計算耗時:0:00:48.608656 結果:12586269025 複製程式碼
上面的計算要耗時48秒,必須要在新的Isolate中做計算
-
準備入參,呼叫spawn建立新的Isolate。
void main() async { Isolate newIsolate = await Isolate.spawn(task1,10,debugName: "isolateDebug"); print("結束!"); } 複製程式碼
輸出結果:
結束! 複製程式碼
在新的Isolate中計算得到的結果沒有列印到當前Console中,此時就要使用ReceivePort與SendPort來建立當前Isolate和新Isolate的連線。
單向通訊
定義可以接收宿主的SendPort的耗時任務,在計算完成後通過send方法將結果發回宿主Isolate。
/// [hostSendPort] 用於isolate向宿主isolate傳送結果
void task2(SendPort hostSendPort){
DateTime startTime = DateTime.now();
int result = fibonacci(47);
DateTime endTime = DateTime.now();
var state = "計算耗時:${endTime.difference(startTime)} 結果:${result
.toString()}";
hostSendPort.send(state);
}
複製程式碼
在宿主Isolate中要定義用於接收結果的ReceivePort,ReceivePort也是Stream,可以設定監聽來處理收到的結果。
void main() async {
// 定義宿主接受結果的ReceivePort,設定監聽。
ReceivePort hostReceivePort = ReceivePort();
hostReceivePort.listen((message) {
print(message);
});
// 定義向hostReceivePort 傳送資料的hostSendPort
SendPort hostSendPort = hostReceivePort.sendPort;
// hostSendPort.send("message"); 測試了在當前isolate中傳送結果的場景。
Isolate newIsolate =
await Isolate.spawn(task2, hostSendPort);
}
複製程式碼
計算結果:
計算耗時:0:00:11.959955 結果:2971215073
複製程式碼
雙向通訊
通過上面的改造,將宿主中的SendPort傳遞給子Isolate,在計算出結果後發回給宿主Isolate,宿主Isolate通過ReceivePort設定監聽來處理結果。就完成了子Isolate向宿主Isolate傳送資料。
但宿主Isolate如何向子Isolate傳送資料呢?
子Isolate向宿主Isolate傳送資料是通過,持有宿主中的SendPort來實現的。那麼要實現宿主Isolate向子Isolate傳送資料,宿主中也得持有子Isolate中的SendPort才行。最簡單的方案就是在子Isolate中建立subSendPort並傳遞迴宿主。
void task3(SendPort hostSendPort) {
///5.建立子Isolate自己的ReceivePort,用於接收宿主傳過來的初始化引數
ReceivePort subReceivePort = ReceivePort();
subReceivePort.listen((start) {
if (start is int) {
///9. 收到宿主中初始化引數後進行計算。
DateTime startTime = DateTime.now();
int result = fibonacci(start);
DateTime endTime = DateTime.now();
var state =
"計算耗時:${endTime.difference(startTime)} 結果:${result.toString()}";
///10.計算結束後通過宿主的hostSendPort將結果發出去。
hostSendPort.send(state);
}
});
///6.將子Isolate自身的sendPort發給宿主,用於宿主向子Isolate傳遞初始化引數。
hostSendPort.send(subReceivePort.sendPort);
}
void main() async {
///1 定義宿主接受結果的port和傳送引數的port
ReceivePort hostReceivePort = ReceivePort();
///2 定義子Isolate的SendPort引用
SendPort subSendPort;
///3 定義監聽,監聽的部分暫時不會執行
hostReceivePort.listen((message) {
if (message is SendPort) {
/// 7.收到子Isolate的SendPort,此時完成了雙向通訊的配置階段
subSendPort = message;
/// 8.向子Isolate傳送計算任務的初始化資料
subSendPort.send(2);
subSendPort.send(10);
subSendPort.send(20);
subSendPort.send(30);
subSendPort.send(40);
subSendPort.send(48);
} else if (message is String) {
/// 11.列印子Isolate中計算的結果。
print(message);
} else {
print("收到的資料不符合規範");
}
});
///4 定義向hostReceivePort 傳送資料的hostSendPort,開始建立
SendPort hostSendPort = hostReceivePort.sendPort;
Isolate newIsolate = await Isolate.spawn(task3, hostSendPort);
}
複製程式碼
最終輸出結果為:
計算耗時:0:00:00.000000 結果:1
計算耗時:0:00:00.000000 結果:55
計算耗時:0:00:00.000000 結果:6765
計算耗時:0:00:00.002999 結果:832040
計算耗時:0:00:00.393017 結果:102334155
計算耗時:0:00:19.179601 結果:4807526976
複製程式碼
上面的程式碼按順序看可能有點繞,新增了註釋描述了基本的執行順序。可以看出要完成一個基本的雙向通訊功能,要寫大段的配置的程式碼,真正屬於業務部分的程式碼就幾行,用起來很繁瑣。
簡化使用
Flutter中對Isolate的使用進行了簡化,通過compute方法可以很方便的實現雙向通訊。
import 'package:flutter/foundation.dart';
int fibonacci(int n) {
return n < 2 ? n : fibonacci(n - 2) + fibonacci(n - 1);
}
void main()async{
var result = await compute( fibonacci,20);
print("計算結果為:$result");
}
複製程式碼
輸出結果:
I/flutter ( 9899): 計算結果為:6765
複製程式碼
引用: