Dart異步Future與事件循環Event Loop

如何使用異步Future

什麼是異步

若是你的程序中有兩個方法,這兩個方法桉順序執行,第一個方法執行須要五秒,若是是同步代碼,第二個方法會等待第一個方法執行完,纔會被調用,html

若是第一個方法是異步的,程序在執行第一個方法時,不會等待它執行結束,而是接着執行第二個方法,這樣第二個方法就無需在第一個方法執行完以後被調用。git

在客戶端異步是很是有用的,若是你在初始化時有一個很是耗時,但又不須要它在ui畫面響應前執行完成的方法,你就可使用異步。github

Dart異步處理庫Future

瞭解了異步的概念後,咱們來看一看如何在Dart中使用異步。web

testFuture();
    testFuture2();
    
  Future testFuture() {
    //下面是一個耗時三秒的任務
    return Future.delayed(Duration(seconds: 3), () => print('異步方法'));
  }

  testFuture2() {
    print("普通方法");
  }

控制檯輸出api

在這裏插入圖片描述

將一個方法的返回值聲明爲Future這樣這個方法就是異步的了。瀏覽器

Future的構造方法

你也可使用Future類的構造方法來使用異步架構

Future(() {
      print('異步方法');
    });

Future類的構造方法以下app

普通的Future類構造
Future(FutureOr<T> computation())

建立一個延遲幾秒執行的Future  duration參數來控制延遲多久
Future.delayed(Duration duration, [FutureOr<T> computation()])

經過微任務隊列處理的Future
Future.microtask(FutureOr<T> computation())

當即返回結果的Future
Future.sync(FutureOr<T> computation())

Future.value([FutureOr<T>? value])

Future.error(Object error, [StackTrace? stackTrace])

構造方法演示框架

Future.delayed(Duration(seconds: 3), () => print('異步方法1'));
Future(() {
  print('異步方法2');
});
Future.microtask(() => print('異步方法3'));
Future.sync(() => print('異步方法4'));

控制檯輸出以下異步

在這裏插入圖片描述

看到不一樣構造方法的執行順序,想必你已經對不一樣的構造方法有所瞭解

值得一提的是Future全部的構造方法返回的都是Future對象,咱們能夠進行鏈式調用

Future的鏈式調用

當 future 執行完成後,then() 中的代碼會被執行。

Future(() {
  print('異步方法');
}).then((value) => print('異步方法2'));

等待多個 Future

有時代碼邏輯須要調用多個異步函數, 並等待它們所有完成後再繼續執行。 使用 Future.wait() 靜態方法管理多個 Future 以及等待它們完成:

Future deleteLotsOfFiles() async =>  ...
Future copyLotsOfFiles() async =>  ...
Future checksumLotsOfOtherFiles() async =>  ...

 Future.wait([
  deleteLotsOfFiles(),
  copyLotsOfFiles(),
  checksumLotsOfOtherFiles(),
]);

簡化Future

使用async和awiat來簡化異步代碼

這樣聲明一個方法 它就是異步的了

testFuture5() async {
    Future.delayed(Duration(seconds: 3), () => print('異步方法1'));
  }

你也能夠像寫同步代碼同樣使用異步,當在async聲明的方法中使用await時,async聲明的方法會等待await修飾的方法執行結束

testFuture2() {
    print("普通方法");
  }
  
  testFuture5() async {
   await Future.delayed(Duration(seconds: 3), () => print('異步方法1'));
  }

  test()async{
   await testFuture5();
    testFuture2();
  }

控制檯輸出以下

image-20201213154650011

若是你對以上的默寫代碼執行順序有所疑惑,不要着急,下面的內容會解答你的全部問題。

事件循環基本概念

本文描述了Dart的事件循環架構,您就能夠編寫出更好的更少問題的異步代碼。您將學習如何使用Future,而且可以預測程序的執行順序。

若是你寫過UI代碼,你可能已經熟悉了事件循環和事件隊列的概念。它們確保了圖形操做和事件(如鼠標點擊)一次只處理一個。

事件循環和隊列

事件循環的工做是從事件隊列中獲取一個事件並處理它,只要隊列中有事件,就重複這兩個步驟。

events going into a queue, feeding into an event loop

隊列中的事件可能表明用戶輸入,文件I / O通知,計時器等。 例如,下面是事件隊列的圖片,其中包含計時器和用戶輸入事件:

same figure, but with explicit events: 1. key, 2.click, 3. timer, etc.

你可能在其餘的語言中熟悉這些。如今咱們來談談dart語言是如何實現的。

Dart的單線程

一旦一個Dart函數開始執行,它將繼續執行直到退出。換句話說,Dart函數不能被其餘Dart代碼打斷。

以下圖所示,一個Dart程序開始執行的第一步是主isolate執行main()函數,當main()退出後,主isolate線程開始逐個處理程序事件隊列上的全部事件。 在這裏插入圖片描述

實際上,這有點過於簡化了。

dart的事件循環和隊列

Dart應用程序的事件循環帶有兩個隊列——事件隊列和微任務隊列。

事件隊列包含全部外部事件:I/O、鼠標事件、繪圖事件、計時器、Dart isolate之間的通訊,等等。

微任務隊列是必要的,由於事件處理代碼有時須要稍後完成一個任務,但在將控制權返回到事件循環以前。例如,當一個可觀察對象發生變化時,它將幾個突變變化組合在一塊兒,並同步地報告它們。微任務隊列容許可觀察對象在DOM顯示不一致狀態以前報告這些突變變化。

事件隊列包含來自應用程序中的事件,微任務隊列只包含來自Dart核心代碼的事件。

以下圖所示,當main()函數退出時,事件循環開始工做。首先,它以FIFO(先進先出)順序執行全部微任務。而後,它使事件隊列中的第一項出隊並處理,而後它重複這個循環:執行全部微任務,而後處理事件隊列上的下一事件。一旦兩個隊列都爲空而且不會再發生任何事件,應用程序的嵌入程序(如瀏覽器或測試框架)就能夠釋放應用程序。

<u>注意:若是web應用程序的用戶關閉了它的窗口,那麼web應用程序可能會在其事件隊列爲空以前強行退出。</u>

flowchart: main() -> microtasks -> next event -> microtasks -> ...

重要:當事件循環正在執行微任務隊列中的任務時,事件隊列會卡住:應用程序沒法繪製圖形、處理鼠標點擊、對I/O作出反應等。

儘管能夠預測任務執行的順序,但不能準確預測事件循環什麼時候將任務從隊列中移除。Dart事件處理系統基於單線程循環;它不是基於任何類型的時間標準。例如,當您建立一個延遲的任務時,事件將在您指定的時間進入隊列。他仍是要等待事件隊列中它以前的全部事件(包括微任務隊列中的每個事件)所有執行完後,才能獲得執行。(延時任務不是插隊,是在指定時間進入隊列)

提示:鏈式調用future指定任務順序

若是您的代碼有依賴關係,請以顯式的方式編寫。顯式依賴關係幫助其餘開發人員理解您的代碼,而且使您的程序能方便的重構。

下面是一個錯誤編碼方式的例子:

// 由於在設置變量和使用變量之間沒有明確的依賴關係,因此很差。
future.then((){...設置一個重要變量...)。
Timer.run(() {...使用重要變量...})。

相反,像這樣寫代碼:

//更好,由於依賴關係是顯式的。

future.then(…設置一個重要的變量…)

then((_){…使用重要的變量…});

在使用該變量以前必須先設置它。(若是您但願即便出現錯誤也能執行代碼,那麼可使用whenComplete()而不是then()。)

若是使用變量須要時間而且能夠在之後完成,請考慮將代碼放在新的Future中:

//可能更好:顯式依賴加上延遲執行。

future.then(…設置一個重要的變量…)

then((_) {new Future((){…使用重要的變量…})});

使用新的Future使事件循環有機會處理事件隊列中的其餘事件。下一節將詳細介紹延遲運行的調度代碼。

如何安排任務

當您須要指定一些須要延遲執行的代碼時,可使用dart:async庫提供的如下API:

Future類,它將一個項目添加到事件隊列的末尾。

頂級的scheduleMicrotask()函數,它將一個項目添加到微任務隊列的末尾。

使用這些api的示例在下一節中。事件隊列:new Future()和微任務隊列:scheduleMicrotask()

使用適當的隊列(一般是事件隊列)

儘量的在事件隊列上調度任務,使用Future。使用事件隊列有助於保持微任務隊列較短,減小微任務隊列影響事件隊列的可能。

若是一個任務須要在處理任何來自事件隊列的事件以前完成,那麼你一般應該先執行該函數。若是不能先執行,那麼使用 scheduleMicrotask()將這個任務添加到微任務隊列中。

shows chain of event handler execution, with tasks added using Future and scheduleMicrotask().

事件隊列: new Future()

要在事件隊列上調度任務,可使用new Future()或new Future.delayed()。這是dart:async庫中定義的兩個Future的構造函數。

注意:您也可使用Timer安排任務,可是若是Timer任務中發生任何未捕獲的異常,您的應用程序將退出。 相反,咱們建議使用Future,它創建在Timer之上,並增長了諸如檢測任務完成和對錯誤進行響應的功能。

要當即將一個事件放到事件隊列中,使用new Future():

//在事件隊列中添加任務。

new Future((){

 /……代碼就在這裏……

});

您能夠添加對then()或whenComplete()的調用,以便在新的Future完成後當即執行一些代碼。例如,當new Future的任務離開隊列時,如下代碼輸出「42」:

new Future(() => 21)
    .then((v) => v*2)
    .then((v) => print(v));

使用new Future.delayed()在一段時間後在隊列中加入一個事件:

// 一段時間以後,將事件加入隊列
new Future.delayed(const Duration(seconds:1), () {
  // ...代碼在這裏...
});

儘管前面的示例在一秒後將任務添加到事件隊列中,但該任務只有在主isolate空閒、微任務隊列爲空以及以前在事件隊列中入隊的任務所有執行完後才能執行。例如,若是main()函數或事件處理程序正在運行一個複雜的計算,則任務只有在該計算完成後才能執行。在這種狀況下,延遲可能遠不止一秒。

關於future的重要細節:

1 傳遞給Future的then()方法的函數在Future完成時當即執行。(函數沒有進入隊列,只是被調用了)

2 若是Future在調用then()以前已經完成,則將一個任務添加到微任務隊列,而後該任務執行傳遞給then()的函數。

3 Future()和Future.delayed()構造函數不會當即完成; 他們將一個項目添加到事件隊列。

4 value()構造函數在微任務中完成,相似於#2

5 Future.sync()構造函數當即執行其函數參數,而且(除非該函數返回Future,若是返回future代碼會進入事件隊列)在微任務中完成,相似於#2。(Future.sync(FutureOr<T> computation())該函數接受一個function參數)

微任務隊列:scheduleMicrotask()

async庫將scheduleMicrotask()定義爲一個頂級函數。你能夠像這樣調用scheduleMicrotask():

scheduleMicrotask(() {
  // ...代碼在這裏...
});

dart2.9會將第一次調用scheduleMicrotask()時,將此代碼插入事件隊列的第一位

向微任務隊列添加任務的一種方法是在已經完成的Future上調用then()。有關更多信息,請參閱前一節

必要時使用isolates 或workers

如今您已經閱讀了關於調度任務的全部內容,讓咱們測試一下您的理解。

請記住,您不該該依賴Dart的事件隊列實現來指定任務順序。 實現可能會發生變化,Future的then()和whenComplete()方法是更好的選擇。 不過,若是您能正確回答下面這些問題,你學會了。

練習

Question #1

這個示例打印出什麼?

import 'dart:async';
void main() {
  print('main #1 of 2');
  scheduleMicrotask(() => print('microtask #1 of 2'));

  new Future.delayed(new Duration(seconds:1),
                     () => print('future #1 (delayed)'));
  new Future(() => print('future #2 of 3'));
  new Future(() => print('future #3 of 3'));

  scheduleMicrotask(() => print('microtask #2 of 2'));

  print('main #2 of 2');
}

答案

main #1 of 2
main #2 of 2
microtask #1 of 2
microtask #2 of 2
future #2 of 3
future #3 of 3
future #1 (delayed)

這個順序應該你能預料到的,由於示例代碼分三批執行:

1 main()函數中的代碼

2 微任務隊列中的任務(scheduleMicrotask())

3 事件隊列中的任務(new Future()或new Future.delayed())

請記住,main()函數中的全部調用都是從頭至尾同步執行的。首先main()調用print(),而後調用scheduleMicrotask(),再調用new Future.delayed(),而後調用new Future(),以此類推。只有回調--做爲 scheduleMicrotask()、new Future.delayed()和new Future()的參數代碼纔會在後面的時間執行

Question #2

這裏有一個更復雜的例子。若是您可以正確地預測這段代碼的輸出,就會獲得一個閃亮的星星。

import 'dart:async';
void main() {
  print('main #1 of 2');
  scheduleMicrotask(() => print('microtask #1 of 3'));

  new Future.delayed(new Duration(seconds:1),
      () => print('future #1 (delayed)'));

  new Future(() => print('future #2 of 4'))
      .then((_) => print('future #2a'))
      .then((_) {
        print('future #2b');
        scheduleMicrotask(() => print('microtask #0 (from future #2b)'));
      })
      .then((_) => print('future #2c'));

  scheduleMicrotask(() => print('microtask #2 of 3'));

  new Future(() => print('future #3 of 4'))
      .then((_) => new Future(
                   () => print('future #3a (a new future)')))
      .then((_) => print('future #3b'));

  new Future(() => print('future #4 of 4'));
  scheduleMicrotask(() => print('microtask #3 of 3'));
  print('main #2 of 2');
}

在這裏插入圖片描述

dart程序會在第一次建立微任務隊列時,將建立微任務隊列的代碼插入到事件隊列的第一位,至關於插隊。

總結

你如今應該瞭解Dart的事件循環以及dart如何安排任務。如下是Dart中事件循環的一些主要概念:

Dart應用程序的事件循環使用兩個隊列執行任務:事件隊列和微任務隊列。

事件隊列有來自Dart(futures、計時器、isolate messages)和系統(用戶操做、I/O等)的事件。

目前,微任務隊列只有來自Dart核心代碼的事件,若是你想讓你的代碼進入微任務隊列執行,使用scheduleMicrotask()。

事件循環在退出隊列並處理事件隊列上的下一項以前先清空微任務隊列。

一旦兩個隊列都爲空,應用程序就完成了它的工做,而且(取決於它的嵌入程序)能夠退出。

main()函數和來自微任務和事件隊列的全部項目都運行在Dart應用程序的主isolates 上。

當你安排一項事件時,遵循如下規則:

若是可能,將其放在事件隊列中(使用new Future()或new Future.delayed())。

使用Future的then()或whenComplete()方法指定任務順序。

爲了不耗盡事件循環,請保持微任務隊列儘量短。

爲了保持應用程序的響應性,避免在任何一個事件循環中執行計算密集型任務。

要執行計算密集型任務,請建立額外的isolates 或者 workers。

參考文章: https://dart.cn/articles/archive/event-loop#:~:text=A%20Dart%20app%20has%20a,queue%20and%20the%20microtask%20queue.&text=First,%20it%20executes%20any%20microtasks,item%20on%20the%20event%20queue.

https://api.dart.dev/stable/2.10.4/dart-async/Future-class.html

https://www.dartcn.com/guides/libraries/library-tour#future

文章中全部的測試異步的代碼都在做者的github上,https://github.com/jack0-0wu/flutter_demo。

若是你對Dart flutter 計算機基礎感興趣能夠關注做者,持續分享優質文章。 坐而論道不如起而行之

相關文章
相關標籤/搜索