Dart 異步與多線程

Dart單線程

Dart中的事件循環是單線程的,在流暢性與安全性體驗較好,核心分爲主線程、微任務、宏任務。主線程主要包括業務處理、網絡IO、本地文件IO、異步等事件。dart的單線程中有兩個事件隊列,一個是微任務隊列、一個是事件隊列。git

  • 微任務隊列

微任務隊列包含有 Dart 內部的微任務,主要是經過 scheduleMicrotask 來調度。github

  • 事件隊列

事件隊列包含外部事件,例如 I/O 、 Timer ,繪製事件等等。web

事件循環 Event loop

鏈式指定事件順序

若是你的代碼具備依賴型,最好是顯式的。這有助於開發人員理解您的代碼並使您的代碼更加健壯。安全

想給事件隊列中鏈式添加任務的錯誤操做示範:bash

future.then(...set an important variable...);
Timer.run(() {...use the important variable...});
複製代碼

正確的是:markdown

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. 在微任務和事件任務一樣能夠產生新的 微任務和事件任務,因此須要再次判斷是否存在新的微任務和事件任務。

驗證一下上邊的運行原理,咱們看下下邊的代碼:

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中異步和多線程是分開的,異步只是事件循環中的多事件輪訓的結果,而多線程能夠理解爲真正的併發(多個線程同時作事)。在單個線程中,又分爲微任務和其餘事件隊列,微任務隊列優先級高於其餘事件隊列。

參考

相關文章
相關標籤/搜索