【dart學習】-- Dart之異步編程

一,概述

  編程中的代碼執行,一般分爲同步異步兩種。java

  • 同步
    簡單說,同步就是按照代碼的編寫順序,從上到下依次執行,這也是最簡單的咱們最常接觸的一種形式。可是同步代碼的缺點也顯而易見,若是其中某一行或幾行代碼很是耗時,那麼就會阻塞,使得後面的代碼不能被馬上執行。
  • 異步
    異步的出現正是爲了解決這種問題,它可使某部分耗時代碼不在當前這條執行線路上馬上執行,那究竟怎麼執行呢?最多見的一種方案是使用多線程,也就至關於開闢另外一條執行線,而後讓耗時代碼在另外一條執行線上運行,這樣兩條執行線並列,耗時代碼天然也就不能阻塞主執行線上的代碼了。

  多線程雖然好用,可是在大量併發時,仍然存在兩個較大的缺陷,一個是開闢線程比較耗費資源,線程開多了機器吃不消,另外一個則是線程的鎖問題,多個線程操做共享內存時須要加鎖,複雜狀況下的鎖競爭不只會下降性能,還可能形成死鎖。所以又出現了基於事件的異步模型
  異步模型簡單說就是在某個單線程中存在一個事件循環和一個事件隊列,事件循環不斷的從事件隊列中取出事件來執行,這裏的事件就比如是一段代碼,每當遇到耗時的事件時,事件循環不會停下來等待結果,它會跳過耗時事件,繼續執行其後的事件。當不耗時的事件都完成了,再來查看耗時事件的結果。所以,耗時事件不會阻塞整個事件循環,這讓它後面的事件也會有機會獲得執行。數據庫

  咱們很容易發現,這種基於事件的異步模型,只適合I/O密集型的耗時操做,由於I/O耗時操做,每每是把時間浪費在等待對方傳送數據或者返回結果,所以這種異步模型每每用於網絡服務器併發。若是是計算密集型的操做,則應當儘量利用處理器的多核,實現並行計算。

編程

二,Dart 的事件循環

Dart 是事件驅動的體系結構,該結構基於具備單個事件循環兩個隊列的單線程執行模型。 Dart雖然提供調用堆棧。 可是它使用事件在生產者和消費者之間傳輸上下文。 事件循環由單個線程支持,所以根本不須要同步和鎖定。

Dart 的兩個隊列分別是服務器

  • MicroTask queue 微任務隊列網絡

  • Event queue 事件隊列多線程

  

Dart事件循環執行如上圖所示併發

  1. 先查看MicroTask隊列是否爲空,不是則先執行MicroTask隊列
  2. 一個MicroTask執行完後,檢查有沒有下一個MicroTask,直到MicroTask隊列爲空,纔去執行Event隊列
  3. 在Evnet 隊列取出一個事件處理完後,再次返回第一步,去檢查MicroTask隊列是否爲空

注意:咱們能夠看出,將任務加入到MicroTask中能夠被儘快執行,但也須要注意,當事件循環在處理MicroTask隊列時,Event隊列會被卡住,應用程序沒法處理鼠標單擊、I/O消息等等事件。異步

三,調度任務

注意如下調用的方法,都定義在dart:async庫中。async

  • 將任務添加到MicroTask隊列有兩種方法
    • 1. 使用 scheduleMicrotask 方法添加
    • 2. 使用Future對象添加
      import  'dart:async';
      
      //個人任務隊列
      void
      myTask(){ print("this is my task"); } void main() { # 1. 使用 scheduleMicrotask 方法添加 scheduleMicrotask(myTask); # 2. 使用Future對象添加 new Future.microtask(myTask); }
  • 將任務添加到Event隊列  
    • 使用Future對象添加
      import  'dart:async';
      //個人任務
      void  myTask(){
          print("this is my task");
      }
      
      void  main() {
       # 1. 使用Future對象添加
      new Future(myTask);
      }
  • 編寫代碼驗證以上的結論
    import 'dart:async'
    
    void main() {
    
      print('main Start');
    
      new Future((){
        print('this is my task');
      });
    
      new Future.microtask((){
        print('this is microtask');
      });
    
      print('main Stop');
      
    }

    運行結果:函數

    main start
    main stop
    this is microtask
    this is my task

能夠看到,代碼的運行順序並非按照咱們的編寫順序來的,將任務添加到隊列並不等於馬上執行,它們是異步執行的,當前main方法中的代碼執行完以後,纔會去執行隊列中的任務,且MicroTask隊列運行在Event隊列以前。

四,延時任務

如須要將任務延伸執行,則可以使用Future.delayed方法

new  Future.delayed(new  Duration(seconds:1),(){
    print('task delayed');
});

表示在延遲時間到了以後將任務加入到Event隊列。須要注意的是,這並非準確的,萬一前面有很耗時的任務,那麼你的延遲任務不必定能準時運行。

import  'dart:async';
import  'dart:io';

void  main() {
    print("main start");

    new Future.delayed(new  Duration(seconds:1),(){
        print('task delayed');
    });

    new Future((){
        // 模擬耗時5秒
        sleep(Duration(seconds:5));
        print("5s task");
    });

    print("main stop");
}

運行結果:

main start
main stop
5s task
task delayed

從結果能夠看出,delayed方法調用在前面,可是它顯然並未直接將任務加入Event隊列,而是須要等待1秒以後纔會去將任務加入,但在這1秒之間,後面的new Future代碼直接將一個耗時任務加入到了Event隊列,這就直接致使寫在前面的delayed任務在1秒後只能被加入到耗時任務以後,只有當前面耗時任務完成後,它纔有機會獲得執行。這種機制使得延遲任務變得不太可靠,你沒法肯定延遲任務到底在延遲多久以後被執行。

五,Future 詳解

Future類是對將來結果的一個代理,它返回的並非被調用的任務的返回值。  

//個人任務
void
myTask(){ print("this is my task"); } void main() { Future fut = new Future(myTask);//根據個人任務建立Future對象 }

如上代碼,Future類實例fut並非函數myTask的返回值,它只是代理了myTask函數,封裝了該任務的執行狀態。換種理解方式就是,Future就是一個受你委託的委託人,你將將來要執行的任務交給他,你告知他任務類型是耗時任務,仍是非耗時任務,而後分類放到事件循環中去,當任務完成後,它會第一時間執行回調方法告知你任務完成,或者會等到你委託給他的全部任務都完成了立馬告知你

  • 建立Future

    Future的幾種建立方法

    • Future()
    • Future.microtask()
    • Future.sync()
    • Future.value()
    • Future.delayed()
    • Future.error()

    其中sync是同步方法,任務會被當即執行
    import  'dart:async';
    
    void  main() {
        print("main start");
    
    new  Future.sync((){
        print("sync task");
    });
    
    new  Future((){
        print("async task");
    });
    
        print("main stop");
    }

    運行結果:

    main start
    sync task
    main stop
    async task
  • 註冊回調
    • 使用then註冊回調

        當Future中的任務完成後,咱們每每須要一個回調,這個回調當即執行,不會被添加到事件隊列。
      import 'dart:async';
      
      void main() {
        print("main start");
      
        Future fut =new Future.value(18);
        // 使用then註冊回調
        fut.then((res){
          print(res);
        });
      
       // 鏈式調用,能夠跟多個then,註冊多個回調
        new Future((){
          print("async task");
        }).then((res){
          print("async task complete");
        }).then((res){
          print("async task after");
        });
      
        print("main stop");
      }

      運行結果:

      main start
      main stop
      18
      async task
      async task complete
      async task after
    • 除了then方法,還可使用catchError來處理異常,以下

        new Future((){
          print("async task");
        }).then((res){
          print("async task complete");
        }).catchError((e){
          print(e);
        });
    • 還可使用靜態方法wait 等待多個任務所有完成後回調。

      import 'dart:async';
      
      void main() {
        print("main start");
      
        Future task1 = new Future((){
          print("task 1");
          return 1;
        });
      
        Future task2 = new Future((){
          print("task 2");
          return 2;
        });
          
        Future task3 = new Future((){
          print("task 3");
          return 3;
        });
      
        Future fut = Future.wait([task1, task2, task3]);
        fut.then((responses){
          print(responses);
        });
      
        print("main stop");
      }

      運行結果:

      main start
      main stop
      task 1
      task 2
      task 3
      [1, 2, 3]

      如上,wait返回一個新的Future,當添加的全部Future完成時,在新的Future註冊的回調將被執行。

六,async 和 await

在Dart1.9中加入了asyncawait關鍵字,有了這兩個關鍵字,咱們能夠更簡潔的編寫異步代碼,而不須要調用Future相關的API。他們容許你像寫同步代碼同樣寫異步代碼和不須要使用Future接口。

async 關鍵字做爲方法聲明的後綴時,具備以下意義

  • 被修飾的方法會將一個 Future 對象做爲返回值
  • 該方法會同步執行其中的方法的代碼直到第一個 await 關鍵字,而後它暫停該方法其餘部分的執行;
  • 一旦由 await 關鍵字引用的 Future 任務執行完成,await的下一行代碼將當即執行。
    // 導入io庫,調用sleep函數
    import 'dart:io';
    
    // 模擬耗時操做,調用sleep函數睡眠2秒
    doTask() async{
      await sleep(const Duration(seconds:2));
      return "Ok";
    }
    
    // 定義一個函數用於包裝
    test() async {
      var r = await doTask();
      print(r);
    }
    
    void main(){
      print("main start");
      test();
      print("main end");
    }

運行結果:

main start
main end
Ok

注意:須要注意,async 不是並行執行,它是遵循Dart 事件循環規則來執行的,它僅僅是一個語法糖,簡化Future API的使用。

  • 總結:

     

    • Future中的then並無建立新的Event丟到Event Queue中,而只是一個普通的Function Call,在FutureTask執行完後,當即開始執行

       

    • 當Future在then函數先已經執行完成了,則會建立一個task,將該task的添加到microtask queue中,而且該任務將會執行經過then傳入的函數

    • Future只是建立了一個Event,將Event插入到了Event Queue的隊尾

    • 使用Future.value構造函數的時候,就會和第二條同樣,建立Task丟到microtask Queue中執行then傳入的函數

    • Future.sync構造函數執行了它傳入的函數以後,也會當即建立Task丟到microtask Queue中執行

七,Isolate

    前面已經說過,將很是耗時的任務添加到事件隊列後,仍然會拖慢整個事件循環的處理,甚至是阻塞。可見基於事件循環的異步模型仍然是有很大缺點的,這時候咱們就須要Isolate,這個單詞的中文意思是隔離。

    簡單說,能夠把它理解爲Dart中的線程。但它又不一樣於線程,更恰當的說應該是微線程,或者說是協程。它與線程最大的區別就是不能共享內存,所以也不存在鎖競爭問題,兩個Isolate徹底是兩條獨立的執行線,且每一個Isolate都有本身的事件循環,它們之間只能經過發送消息通訊,因此它的資源開銷低於線程。

   isolate自己是隔離的意思,有本身的內存和單線程控制的實體,由於 isolate之間的內存在邏輯是隔離的, isolate的代碼是按順序執行的。在 Dart併發可使用用 isolateisolateThread很像,可是 isolate之間沒有共享內存。一個 Dart程序是在 Main isolate的Main函數開始,咱們平時開發中,默認環境就是 Main isolate,App的啓動入口 main函數就是一個 isolate,在Main函數結束後, Main isolate線程開始一個一個處理 Event Queue中的每個 Event


從主Isolate建立一個新的Isolate有兩種方法

  • spawnUri
    static Future<Isolate> spawnUri()
    

    spawnUri方法有三個必須的參數,
      第一個是Uri,指定一個新Isolate代碼文件的路徑,
          第二個是參數列表,類型是List<String>,
          第三個是動態消息。
    須要注意,用於運行新Isolate的代碼文件中,必須包含一個main函數,它是新Isolate的入口方法,該main函數中的args參數列表,正對應spawnUri中的第二個參數。如不須要向新Isolate中傳參數,該參數可傳空List

    Isolate中的代碼:

    import 'dart:isolate'; 
    
    void main() {
      print("main isolate start");
      create_isolate();
      print("main isolate stop");
    }
    
    // 建立一個新的 isolate
    create_isolate() async{
      ReceivePort rp = new ReceivePort();
      SendPort port1 = rp.sendPort;
    
      Isolate newIsolate = await Isolate.spawnUri(new Uri(path: "./other_task.dart"), ["hello, isolate", "this is args"], port1);
    
      SendPort port2;
      rp.listen((message){
        print("main isolate message: $message");
        if (message[0] == 0){
          port2 = message[1];
        }else{
          port2?.send([1,"這條信息是 main isolate 發送的"]);
        }
      });
    
      // 能夠在適當的時候,調用如下方法殺死建立的 isolate
      // newIsolate.kill(priority: Isolate.immediate);
    }

    建立other_task.dart文件,編寫新Isolate的代碼

    import 'dart:isolate';
    import  'dart:io';
    
    
    void main(args, SendPort port1) {
      print("isolate_1 start");
      print("isolate_1 args: $args");
    
      ReceivePort receivePort = new ReceivePort();
      SendPort port2 = receivePort.sendPort;
    
      receivePort.listen((message){
        print("isolate_1 message: $message");
      });
    
      // 將當前 isolate 中建立的SendPort發送到主 isolate中用於通訊
      port1.send([0, port2]);
      // 模擬耗時5秒
      sleep(Duration(seconds:5));
      port1.send([1, "isolate_1 任務完成"]);
    
      print("isolate_1 stop");
    }

    運行主Isolate的結果:

    main isolate start
    main isolate stop
    isolate_1 start
    isolate_1 args: [hello, isolate, this is args]
    main isolate message: [0, SendPort]
    isolate_1 stop
    main isolate message: [1, isolate_1 任務完成]
    isolate_1 message: [1, 這條信息是 main isolate 發送的]

    整個消息通訊過程如上圖所示:

      兩個Isolate是經過兩對Port對象通訊,一對Port分別由用於接收消息的ReceivePort對象,和用於發送消息的SendPort對象構成。其中SendPort對象不用單首創建,它已經包含在ReceivePort對象之中。須要注意,一對Port對象只能單向發消息,這就如同一根自來水管,ReceivePort和SendPort分別位於水管的兩頭,水流只能從SendPort這頭流向ReceivePort這頭。所以,兩個Isolate之間的消息通訊確定是須要兩根這樣的水管的,這就須要兩對Port對象。

    理解了Isolate消息通訊的原理,那麼在Dart代碼中,具體是如何操做的呢?

    ReceivePort對象經過調用listen方法,傳入一個函數可用來監聽並處理髮送來的消息。SendPort對象則調用send()方法來發送消息。send方法傳入的參數能夠是null,num, bool, double,String, List ,Map或者是自定義的類。
    在上例中,咱們發送的是包含兩個元素的List對象,第一個元素是整型,表示消息類型,第二個元素則表示消息內容。

  • spawn

    static Future<Isolate> spawn()
    

     除了使用spawnUri,更經常使用的是使用spawn方法來建立新的Isolate,咱們一般但願將新建立的Isolate代碼和main Isolate代碼寫在同一個文件,且不但願出現兩個main函數,而是將指定的耗時函數運行在新的Isolate,這樣作有利於代碼的組織和代碼的複用。spawn方法有兩個必須的參數,第一個是須要運行在新Isolate的耗時函數,第二個是動態消息,該參數一般用於傳送主Isolate的SendPort對象。

    spawn的用法與spawnUri類似,且更爲簡潔,將上面例子稍做修改以下

    import 'dart:isolate'; 
    import  'dart:io';
    
    void main() {
      print("main isolate start");
      create_isolate();
      print("main isolate end");
    }
    
    // 建立一個新的 isolate
    create_isolate() async{
      ReceivePort rp = new ReceivePort();
      SendPort port1 = rp.sendPort;
    
      Isolate newIsolate = await Isolate.spawn(doWork, port1);
    
      SendPort port2;
      rp.listen((message){
        print("main isolate message: $message");
        if (message[0] == 0){
          port2 = message[1];
        }else{
          port2?.send([1,"這條信息是 main isolate 發送的"]);
        }
      });
    }
    
    // 處理耗時任務
    void doWork(SendPort port1){
      print("new isolate start");
      ReceivePort rp2 = new ReceivePort();
      SendPort port2 = rp2.sendPort;
    
      rp2.listen((message){
        print("doWork message: $message");
      });
    
      // 將新isolate中建立的SendPort發送到主isolate中用於通訊
      port1.send([0, port2]);
      // 模擬耗時5秒
      sleep(Duration(seconds:5));
      port1.send([1, "doWork 任務完成"]);
    
      print("new isolate end");
    }

    運行結果:

    main isolate start
    main isolate end
    new isolate start
    main isolate message: [0, SendPort]
    new isolate end
    main isolate message: [1, doWork 任務完成]
    doWork message: [1, 這條信息是 main isolate 發送的]

    不管是上面的spawn仍是spawnUri,運行後都會建立兩個進程,一個是主Isolate的進程,一個是新Isolate的進程,兩個進程都雙向綁定了消息通訊的通道,即便新的Isolate中的任務完成了,它的進程也不會馬上退出,所以,當使用完本身建立的Isolate後,最好調用newIsolate.kill(priority: Isolate.immediate);將Isolate當即殺死。

八,Flutter 中建立Isolate

不管如何,在Dart中建立一個Isolate都顯得有些繁瑣,惋惜的是Dart官方並未提供更高級封裝。可是,若是想在Flutter中建立Isolate,則有更簡便的API,這是由Flutter官方進一步封裝ReceivePort而提供的更簡潔API。詳細API文檔

使用compute函數來建立新的Isolate並執行耗時任務

import 'package:flutter/foundation.dart';
import  'dart:io';

// 建立一個新的Isolate,在其中運行任務doWork
create_new_task() async{
  var str = "New Task";
  var result = await compute(doWork, str);
  print(result);
}


void doWork(String value){
  print("new isolate doWork start");
  // 模擬耗時5秒
  sleep(Duration(seconds:5));

  print("new isolate doWork end");
  return "complete:$value";
}

compute函數有兩個必須的參數,
  第一個是待執行的函數,這個函數必須是一個頂級函數,不能是類的實例方法,能夠是類的靜態方法,
  第二個參數爲動態的消息類型,能夠是被運行函數的參數。
須要注意,使用compute應導入'package:flutter/foundation.dart'包。

  • 使用場景
      Isolate雖好,但也有合適的使用場景,不建議濫用Isolate,應儘量多的使用Dart中的事件循環機制去處理異步任務,這樣才能更好的發揮Dart語言的優點。

      那麼應該在何時使用Future,何時使用Isolate呢?
    一個最簡單的判斷方法是根據某些任務的平均時間來選擇:

      方法執行在幾毫秒或十幾毫秒左右的,應使用Future,若是一個任務須要幾百毫秒或之上的,則建議建立單獨的Isolate
    除此以外,還有一些能夠參考的場景

    • JSON 解碼
    • 加密
    • 圖像處理:好比剪裁
    • 網絡請求:加載資源、圖片

九,參考:

 《Flutter學習之事件循環機制、數據庫、網絡請求》

相關文章
相關標籤/搜索