Dart 非同步與多執行緒

fgyong發表於2020-07-29

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...
});
複製程式碼

瞭解了基本的用法之後,那麼如何把他們搭配使用呢?

使用任務

有了單執行緒和佇列,那必然有對應的迴圈,這樣子才能執行不同的佇列任務和處理事件,那麼我們看下 迴圈。

  1. 進入main函式,併產生相應的微任務和事件佇列
  2. 判斷是否存在微任務,有則執行,沒有則繼續。執行完判斷是否還有微任務,有則執行,沒則繼續
  3. 如果不存在可執行的微任務,則判斷 是否有事件任務,有則執行,無則繼續返回判斷是否存在事件任務
  4. 在微任務和事件任務同樣可以產生新的 微任務和事件任務,所以需要再次判斷是否存在新的微任務和事件任務。

Dart  非同步與多執行緒

驗證一下上邊的執行原理,我們看下下邊的程式碼:

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');
}
複製程式碼

執行過程如下:

  1. 首先啟動main函式,列印start
  2. 執行scheduleMicrotask微任務,新增任務到微任務佇列中
  3. 執行Future事件,給事件佇列新增任務
  4. 執行timer事件,給事件佇列新增任務
  5. 執行事件列印end
  6. 第二次迴圈判斷是否有微任務,剛才已新增微任務,現在執行微任務,列印Microtask 1
  7. 判斷是否有事件任務,剛才已新增Future任務,執行列印Future 1
  8. 判斷是否有事件任務,剛才已新增Timer 1任務,執行列印Timer1

輸出

start
end
Microtask 1
Future 1
Timer 1
複製程式碼

看下面這個例子證明TimerFuture是一個型別的事件。

/// 微任務
/// 定時器
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
複製程式碼

上面的testtest2TimerFuture位置調換了一下,也就是新增事件任務先後順序顛倒了一下,在執行的時候也顛倒了一下。我們再看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
}
複製程式碼

執行順序:

  1. 列印 start
  2. 新增微任務到佇列中
  3. 新增Timer 1到事件佇列
  4. 執行列印 end任務
  5. 判斷是否有微任務,發現微任務Microtask 1,立即執行
  6. 判斷是有有微任務,發現沒有,則判斷是否有事件任務,發現Timer 1任務並執行,新增事件任務Timer 1 Microtask 2,新增微任務Microtask 2到微任務佇列
  7. 判斷是否有微任務,發現微任務Microtask 2,並執行。
  8. 判斷是否有事件任務,發現事件任務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執行緒不共享記憶體,通過訊息機制通訊

我們看個例子,利用DartIsolates實現多執行緒。

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中非同步和多執行緒是分開的,非同步只是事件迴圈中的多事件輪訓的結果,而多執行緒可以理解為真正的併發(多個執行緒同時做事)。在單個執行緒中,又分為微任務和其他事件佇列,微任務佇列優先順序高於其他事件佇列。

參考

相關文章