Dart單執行緒
Dart
中的事件迴圈是單執行緒的,在流暢性與安全性體驗較好,核心分為主執行緒、微任務、巨集任務。主執行緒主要包括業務處理、網路IO、本地檔案IO、非同步等事件。dart的單執行緒中有兩個事件佇列,一個是微任務佇列、一個是事件佇列。
- 微任務佇列
微任務佇列包含有 Dart 內部的微任務,主要是通過 scheduleMicrotask 來排程。
- 事件佇列
事件佇列包含外部事件,例如 I/O 、 Timer ,繪製事件等等。
事件迴圈 Event loop
鏈式指定事件順序
如果你的程式碼具有依賴型,最好是顯式的。這有助於開發人員理解您的程式碼並使您的程式碼更加健壯。
想給事件佇列中鏈式新增任務的錯誤操作示範:
future.then(...set an important variable...);
Timer.run(() {...use the important variable...});
複製程式碼
正確的是:
future.then(...set an important variable...)
.then((_) {...use the important variable...});
複製程式碼
在Dart
單執行緒中,他們的結果都是一致的,但是理解起來難易程度不是一致的。
如何建立任務
當你想稍後在任務中執行你的程式碼,可以使用Future
類,它將把任務新增到事件佇列末尾,或者使用頂級函式scheduleMicrotask
把任務新增到微任務佇列的末尾。
為了更好的使用then
,或者在發生錯誤時也需要執行的話,那麼請在whenComplate()
代替then
.
如何新增任務到事件佇列
可以使用Timer
或者Future
都可以
您也可以使用Timer計劃任務,但是如果任務中發生任何未捕獲的異常,則應用程式將退出。相反,我們建議使用Future,它建立在Timer之上,並增加了諸如檢測任務完成和對錯誤進行響應的功能。
新增程式碼到事件佇列
new Future(() {
// ...code goes here...
});
複製程式碼
你可以使用then
或者whenComplate
執行後邊的任務,看下這個例子
new Future(() => 21)
.then((v) => v*2)
.then((v) => print(v));
複製程式碼
如果你想稍後再執行的話,請使用Future.delayed()
:
new Future.delayed(const Duration(seconds:1), () {
// ...code goes here...
});
複製程式碼
瞭解了基本的用法之後,那麼如何把他們搭配使用呢?
使用任務
有了單執行緒和佇列,那必然有對應的迴圈,這樣子才能執行不同的佇列任務和處理事件,那麼我們看下 迴圈。
- 進入
main
函式,併產生相應的微任務和事件佇列 - 判斷是否存在微任務,有則執行,沒有則繼續。執行完判斷是否還有微任務,有則執行,沒則繼續
- 如果不存在可執行的微任務,則判斷 是否有事件任務,有則執行,無則繼續返回判斷是否存在事件任務
- 在微任務和事件任務同樣可以產生新的 微任務和事件任務,所以需要再次判斷是否存在新的微任務和事件任務。
驗證一下上邊的執行原理,我們看下下邊的程式碼:
void main() {
test();
}
/// 微任務
/// 定時器
void test() async {
print('start');
scheduleMicrotask(() {
print('Microtask 1');
});
Future.delayed(Duration(seconds: 0)).then((value) {
print('Future 1');
});
Timer.run(() {
print('Timer 1');
});
print('end');
}
複製程式碼
執行過程如下:
- 首先啟動
main
函式,列印start
- 執行
scheduleMicrotask
微任務,新增任務到微任務佇列中 - 執行
Future
事件,給事件佇列新增任務 - 執行
timer
事件,給事件佇列新增任務 - 執行事件列印
end
- 第二次迴圈判斷是否有微任務,剛才已新增微任務,現在執行微任務,列印
Microtask 1
- 判斷是否有事件任務,剛才已新增
Future
任務,執行列印Future 1
- 判斷是否有事件任務,剛才已新增
Timer 1
任務,執行列印Timer1
輸出
start
end
Microtask 1
Future 1
Timer 1
複製程式碼
看下面這個例子證明Timer
和Future
是一個型別的事件。
/// 微任務
/// 定時器
void test2() async {
print('start');
scheduleMicrotask(() {
print('Microtask 1');
});
Timer.run(() {
print('Timer 1');
});
Future.delayed(Duration(seconds: 0)).then((value) {
print('Future 1');
});
print('end');
}
複製程式碼
輸出
start
end
Microtask 1
Timer 1
Future 1
複製程式碼
上面的test
和test2
中Timer
和Future
位置調換了一下,也就是新增事件任務先後順序顛倒了一下,在執行的時候也顛倒了一下。我們再看Future
原始碼:
factory Future.delayed(Duration duration, [FutureOr<T> computation()]) {
_Future<T> result = new _Future<T>();
new Timer(duration, () {
if (computation == null) {
result._complete(null);
} else {
try {
result._complete(computation());
} catch (e, s) {
_completeWithErrorCallback(result, e, s);
}
}
});
return result;
}
複製程式碼
Future.delayed
原始碼本質就是將任務新增到Timer
中,在指定時間後執行該任務。
執行完事件需要再次判斷是否有微任務
微任務佇列優先順序高於事件佇列,所以每次執行任務首先判斷是否存在未執行的微任務。
事件佇列執行完,有可能有新的微任務被新增到佇列中,所以還需要掃描微任務佇列一次。 看下面的例子:
void test3() async {
print('start'); //1
scheduleMicrotask(() {//2
print('Microtask 1');//5
});
Timer.run(() {//3
print('Timer 1');//6
Timer.run(() {//9
print('Timer 1 Microtask 2 ');//10
});
scheduleMicrotask(() {//7
print('Microtask 2');//8
});
});
print('end');//4
}
複製程式碼
執行順序:
- 列印
start
- 新增微任務到佇列中
- 新增
Timer 1
到事件佇列 - 執行列印
end
任務 - 判斷是否有微任務,發現微任務
Microtask 1
,立即執行 - 判斷是有有微任務,發現沒有,則判斷是否有事件任務,發現
Timer 1
任務並執行,新增事件任務Timer 1 Microtask 2
,新增微任務Microtask 2
到微任務佇列 - 判斷是否有微任務,發現微任務
Microtask 2
,並執行。 - 判斷是否有事件任務,發現事件任務
Timer 1 Microtask 2
並執行。
結果輸出
start
end
Microtask 1
Timer 1
Microtask 2
Timer 1 Microtask 2
複製程式碼
微任務或者事件任務會卡嗎?
根據前邊的瞭解,執行完微任務再執行事件任務,當某個事件處理時間需要很長,則後邊的任務則會一直處於等待狀態。下邊我們看一個例子,當任務足夠都,還是需要一定時間去處理的。
void test4() async {
print('start ${DateTime.now()}');
for(int i =0;i < 99999;i++){
scheduleMicrotask(() {
print('Microtask 1');
});
}
Timer.run(() {
print('Timer 1 ${DateTime.now()}');
});
print('end ${DateTime.now()}');
}
複製程式碼
輸出:
start 2020-07-28 17:44:11.561886
end 2020-07-28 17:44:11.593989
...
Microtask 1
.....
Timer 1 2020-07-28 17:44:11.893093
複製程式碼
可以看出這些任務執行完成耗時基本達到了0.33
秒。
當一個執行緒出現處理任務不夠了,那麼就需要在開啟一個執行緒了。
Isolates 多執行緒
上邊的 dart
進入main函式是單執行緒的,在Dart中,多執行緒叫做Isolates
執行緒,每個Isolates執行緒不共享記憶體,通過訊息機制通訊。
我們看個例子,利用Dart
的Isolates
實現多執行緒。
void test5()async{
final rece = ReceivePort();
isolate = await Isolate.spawn(sendPort, rece.sendPort);
rece.listen((data){
print('收到了 ${data} ,name:$name');
});
}
void sendPort(SendPort sendPort){
sendPort.send('傳送訊息');
}
Isolate isolate;
String name='fgyong';
void main() {
test5();
}
複製程式碼
輸出
收到了 傳送訊息 ,name:fgyong
複製程式碼
多執行緒相互溝通怎麼處理?
建立執行緒之後子執行緒需要傳送主執行緒一個埠和訊息,主執行緒記錄該埠,下次和子執行緒通訊使用該埠即可。
具體程式碼如下:
/// 新執行緒執行新的任務 並監聽
Isolate isolate;
Isolate isolate2;
void createTask() async {
ReceivePort receivePort = ReceivePort();
isolate = await Isolate.spawn(sendP1, receivePort.sendPort);
receivePort.listen((data) {
print(data);
if (data is List) {
SendPort subSencPort = (data as List)[1];
String msg = (data as List)[0];
print('$msg 在主執行緒收到');
if (msg == 'close') {
receivePort.close();
} else if (msg == 'task') {
taskMain();
}
subSencPort.send(['主執行緒發出']);
}
});
}
void sendP1(SendPort sendPort) async {
ReceivePort receivePort = new ReceivePort();
receivePort.listen((data) async {
print(data);
if (data is List) {
String msg = (data as List)[0];
print('$msg 在子執行緒收到');
if (msg == 'close') {
receivePort.close();
} else if (msg == 'task') {
var m = await task();
sendPort.send(['$m', receivePort.sendPort]);
}
}
});
sendPort.send(['子執行緒執行緒發出', receivePort.sendPort]);
}
Future<String> task() async {
print('子執行緒執行task');
for (var i = 0; i < 99999999; i++) {}
return 'task 完成';
}
void taskMain() {
print('主執行緒執行task');
}
複製程式碼
輸出:
[子執行緒執行緒發出, SendPort]
子執行緒執行緒發出 在主執行緒收到
[主執行緒發出]
主執行緒發出 在子執行緒收到
複製程式碼
更多子執行緒與主執行緒互動請上程式碼庫檢視
複雜問題解決方案
假設一個專案,需要 2 個團隊去完成,團隊中包含多項任務。可以分為 2 個高優先順序任務(高優先順序的其中,會產生2個任務,一個是緊急一個是不緊急),和 2 個非高優先順序任務(非高優先順序的其中,會產生有 2 個任務,一個是緊急一個是不緊急)。其中還有一個是必須依賴其他團隊去做的,因為本團隊沒有那方面的資源,第三方也會產生一個高優先順序任務和一個低優先順序任務。
根據緊急任務作為微任務,非緊急任務作為事件任務來安排,第三方是新開執行緒
主任務 | 高優先順序(微任務) | 低優先順序(事件任務) | 第三方(Isolate) |
---|---|---|---|
H1 | H1-1 | L1-2 | 否 |
H2 | H2-1 | L2-2 | 否 |
L3 | H3-1 | L3-2 | 否 |
L4 | H4-1 | L4-2 | 否 |
I5 | IH5-1 | I5-2 | 是 |
void test6() {
createTask();//建立執行緒
scheduleMicrotask(() {//第一個微任務
print('H1');
scheduleMicrotask(() {//第一個緊急任務
print('H1-1');
});
Timer.run(() {//第一個非緊急任務
print('L1-2');
});
});
scheduleMicrotask(() {// 第二個高優先順序任務
print('H2');
scheduleMicrotask(() {//第二個緊急任務
print('H2-1');
});
Timer.run(() {//第二個非緊急任務
print('L2-2');
});
});
Timer.run(() {// 第一個低優先順序任務
print('L3');
scheduleMicrotask(() {//第三個緊急任務
print('H3-1');
});
Timer.run(() {//第三個非緊急任務
print('L3-2');
});
});
Timer.run(() {// 第二個低優先順序任務
print('L4');
scheduleMicrotask(() {//第四個緊急任務
print('H4-1');
});
Timer.run(() {//第四個非緊急任務
print('L4-2');
});
});
}
/// 新執行緒執行新的任務 並監聽
Isolate isolate;
void createTask() async {
ReceivePort receivePort = ReceivePort();
isolate = await Isolate.spawn(sendPort, receivePort.sendPort);
receivePort.listen((data) {
print(data);
});
}
/// 新執行緒執行任務
void sendPort(SendPort sendPort) {
scheduleMicrotask(() {
print('IH5-1');
});
Timer.run(() {
print('IL5-2');
});
sendPort.send('第三方執行任務結束');
}
複製程式碼
執行結果
H1
H2
H1-1
H2-1
L3
H3-1
L4
H4-1
L1-2
L2-2
L3-2
L4-2
IH5-1
IL5-2
第三方執行任務結束
複製程式碼
可以看到H
開頭的為高優先順序,L
開頭為低優先順序,基本高優先順序執行都在低優先順序之前,符合預期。
但是第三方的為什麼在最後才執行了?
由於建立執行緒需要事件,其他任務均為耗時太短,那麼我們重新做一個耗時事件長的任務即可。
createTask();//建立執行緒
for (var i = 0; i < 9999999999; i++) {
}
...
複製程式碼
輸出:
IH5-1
IL5-2
H1
H2
H1-1
H2-1
第三方執行任務結束
L3
H3-1
L4
H4-1
L1-2
L2-2
L3-2
L4-2
複製程式碼
為什麼 第三方執行任務結束 在正中間?而不是在H1上邊??
因為這個事件屬於低優先順序,而H
開頭的都是高優先順序任務。
為什麼 第三方執行任務結束 ??
在L3
上邊。而不是在最下邊,一定在低優先順序佇列的第一個嗎 ?
由於開始的耗時操作事件太長,導致所有任務執行前,第三方任務已經執行完成,所以 第三方執行任務結束 是第一個新增到低優先順序任務佇列的,所以在低優先順序佇列第一個執行。
當耗時操作比較少時,則 第三方執行任務結束 新增順序則不確定。
總結
Dart
中非同步和多執行緒是分開的,非同步只是事件迴圈中的多事件輪訓的結果,而多執行緒可以理解為真正的併發(多個執行緒同時做事)。在單個執行緒中,又分為微任務和其他事件佇列,微任務佇列優先順序高於其他事件佇列。
參考
- Flutter快用快學
- The Event Loop and Dart
- 程式碼庫 檢視demo