iT邦幫忙

第 12 屆 iThome 鐵人賽

DAY 10
1
自我挑戰組

從零開始的Flutter世界系列 第 10

Day10 Dart 語言介紹<八> 非同步 Asynchrony support

  • 分享至 

  • xImage
  •  

首先我們需要先知道Dart 是單執行緒模型的語言,也就是說程式碼執行的順序是會跟我們編寫的順序一樣,自上而下執行,但是在開發專案中,我們經常會需要做一些耗時的動作,比如說網路請求、檔案讀取等等,此時程式就會需要等待耗時的動作完成才能繼續執行,而這樣就會造成我們畫面UI 的卡頓,那麼下面我們就來看看Dart 是怎麼處理這個問題吧

isolate

isolateDart對多執行緒的實現方案,可以說是Dart的執行緒Thread,但和普通Thread不同的是,isolate是由執行緒和獨立記憶體所構成,正是由於isolate執行緒之間的記憶體不共享,只能依靠訊息機制通訊,所以isolate執行緒之間並不會有資源搶佔的問題,通過isolate我們可以很好的利用多核CPU,來進行大量耗時任務的處理,其中每個isolate都包含一個事件迴圈(Event Loop) 以及兩個事件佇列(MicroTask Queue 以及 Event Queue)

當你執行一個Flutter或任何Dart程式時,將建立並啟動一個預設的isolate,可以稱之為main isolateDart會自動初始化main isolate的兩個先進先出 (FIFO) 的佇列,即MicroTask Queue 以及 Event Queue,然後先執行完Dart的入口方法main()後,事件迴圈 Event Loop 開始啟動,如果不開啟新的isolate,所有的事件都會排在main isolate的事件迴圈中處理

Event Loop處理Event Queue的流程圖:
https://ithelp.ithome.com.tw/upload/images/20210915/20118479Dlr1kTFLG7.png

  • Event Loop

    為一個無限迴圈,它在MicroTask、Event Queue 中查詢是否有需要執行的事件,如果有要執行的事件,且CPU空閒時,Event Loop會取出此事件並執行其對應需要執行的程式碼塊

  • Microtask Queue

    可以自己向isolate內部新增事件,事件的優先順序比Event Queue高,但通常來源於Dart內部,並且非常少,這是因為如果Microtask Queue非常多的話,就會造成Event Queue排不上隊,會阻塞任務佇列的執行 (比如使用者點選沒有反應的情況),為了保證UI 顯示順暢不卡頓,應該儘量將耗時操作放在Event Queue

  • Event Queue

    負責處理I/O事件、繪製事件、手勢事件、接收其他isolate訊息等外部事件,每次外部事件被觸發時,要執行的相應程式碼都會被新增到Event Queue中,當MicroTask Queue中沒有任何內容時,Event Loop才會從Event Queue中取出第一項來處理

    ( 後面會介紹的Future 也會被新增到 Event Queue )

程式執行的流程圖:

https://ithelp.ithome.com.tw/upload/images/20210915/20118479lCOYnUI9fa.png

  1. Dart的入口是main()函式,main()函式中的程式碼會優先執行
  2. 執行完main()後,啟動Event Loop,開始執行佇列中的需要被執行的任務
  3. 檢查MicroTask Queue是否為空,MicroTask Queue不為空的話,按照先進先出的順序去執行最先進來MicroTask Queue的事件
  4. 每次執行完一個MicroTask事件後,再去重複第三步,檢查MicroTask Queue是否為空,若不為空,一樣再照順序去執行事件,直到MicroTask Queue為空,才去執行Event Queue
  5. Event Queue也按照先進先出的順序去執行最先進來Event Queue的事件,每次執行完事件後,再次返回第三步並重複動作直到兩個佇列都沒有事件要執行

那為什麼單一個isolate還是可以實現非同步呢?

這是因為我們的 App 絕大多數時間都是空閒的狀態。比如,等使用者點選、等網路請求回覆、檔案讀寫的I/O,等等,而這些等待的行為並不會阻塞我們的執行緒,這是因為這些網路請求、檔案讀寫的I/O,我們都可以非阻塞式的呼叫這些事件,使這些事件處理都不會阻塞我們單執行緒的繼續執行!

  • 阻塞式呼叫: 呼叫結果返回之前,當前執行緒會被佔用著,只有在得到呼叫結果之後才會繼續執行
  • 非阻塞式呼叫: 呼叫執行之後,當前執行緒不會停止執行,等待的過程中可以執行的事件,等非阻塞式呼叫的事件回調結果後,會再去執行相對應的事件 (比如網路請求,Socket 本身提供了 select 模型可以非同步查詢,而檔案 I/O,作業系統也提供了事件的回撥機制),我們也可以使用DartFuture、async 以及await,來建立一個非同步事件

Future

非同步執行的任務,會在未來某個時間點完成,成功時返回任務執行的結果 (即 Future<T> 的泛型T物件),失敗時則返回錯誤異常

如果我們是同步的對網路做請求 (用 getNetworkData()模擬一個網路請求):

import "dart:io";

main(List<String> args) {
  print("main function start");
  print(getNetworkData());
  print("main function end");
  
  //印出 main function start
	//(停個三秒)
  //印出 network data
  //印出 main function end
}

String getNetworkData() {
  sleep(Duration(seconds: 3));
  return "network data";
}

而上述getNetworkData()會阻塞住我們的main(),只有在得到結果之後才會繼續執行,所以我們要把它修改成非同步的網路請求

使用Future

import "dart:io";

main(List<String> args) {
  print("main function start");
  print(getNetworkData());
  print("main function end");
  
  /*
  印出 main function start
  (沒有出現任何的阻塞現象)
	印出 Instance of 'Future<String>' 
	印出 main function end
	*/
}

Future<String> getNetworkData() {
  return Future<String>(() {
    sleep(Duration(seconds: 3));
    return "network data";
  });
}

透過Future,我們就能夠將解決耗時事件隔離了起來,且不會再佔用住我們的執行緒了,但問題是,我們這樣只是取得一個Future的例項,我們要如何去拿到我們要的結果呢?

答案就是通過Future例項使用.then取得回傳結果

import "dart:io";

main(List<String> args) {
  print("main function start");
  // 使用變數接收getNetworkData返回的future
  var future = getNetworkData();
  // 當future例項有返回結果時,會自動回撥then中傳入的函式
  // 該函式會被放入到事件迴圈中,被執行
  future.then((value) {
    print(value);
  });
  print("main function end");
  
   /*
  印出 main function start
	印出 main function end
  (3秒後)
  印出 network data
	*/
}

Future<String> getNetworkData() {  //Future<T>,.then 會接收返回的泛型T 物件
  return Future<String>(() {
    sleep(Duration(seconds: 3));
    return "network data";
  });
}

那如果在非同步的時候發生異常,會是什麼情況呢?

import "dart:io";

main(List<String> args) {
  print("main function start");
  var future = getNetworkData();
  future.then((value) {
    print(value);
  }).catchError((error) { // 捕獲出現異常時的情況
    print(error); //異常時要做的處理
  });
  print("main function end");
 
  /*
  印出 main function start
	印出 main function end
	(3秒後)
	Exception: 網路請求出現錯誤
  */
}

Future<String> getNetworkData() {
  return Future<String>(() {
    sleep(Duration(seconds: 3));
    // 模擬一個異常出現
    throw Exception("網路請求出現錯誤");
  });
}

Future 的鏈式呼叫

import "dart:io";

main(List<String> args) {
  print("main function start");

  getNetworkData().then((value1) {
    print(value1);
    return "content data2";
  }).then((value2) {
    print(value2);
    return "message data3";
  }).then((value3) {
    print(value3);
  });

  print("main function end");
  
  /*
  印出 main function start
	印出 main function end
	(3秒後)
	印出 network data1
	印出 content data2
	印出 message data3
  */
}

Future<String> getNetworkData() {
  return Future<String>(() {
    sleep(Duration(seconds: 3));
    return "network data1";
  });
}

Future 其他的 API

  1. Future.value(value)

    直接取得一個已完成的Future,會直接呼叫執行.then函式

    main(List<String> args) {
      print("main function start");
    
      Future.value("test value").then((value) {
        print(value);
      });
    
      print("main function end");
    }
    /*印出
    main function start
    main function end
    test value
    */
    
    //test value 比較晚印出是因為Future 的.then 會作為新的事件,加入到Event Queue,故在Event Loap輪到它時才會執行
    
  2. Future.error(object)

    同上,但是是獲得一個發生異常的Future,會直接呼叫執行.catchError函式

    main(List<String> args) {
      print("main function start");
    
      Future.error(Exception("錯誤資訊")).catchError((error) {
        print(error);
      });
    
      print("main function end");
      /*印出
    	main function start
    	main function end
    	Exception: 錯誤資訊
    	*/
    }
    
  3. Future.delayed(時間, 回調函式)

    延遲一定時間後,執行回調函式,執行完回調函式後會再執行then

    main(List<String> args) {
      print("main function start");
    
      Future.delayed(Duration(seconds: 3), () {
        print("delayed");
        return "3秒後的資訊";
      }).then((value) {
        print(value);
      });
    
      print("main function end");
      /*印出
    	main function start
    	main function end
    	delayed
    	3秒後的資訊
    	*/
    }
    

async/await

Future是非同步任務的封裝,藉助於awaitasync,我們可以通過事件迴圈實現非阻塞的同步等待

async修飾過的方法會將一個 Future 物件作為返回值,該方法會同步執行其中的程式法,直到第一個await關鍵字,然後會暫停執行,等await引用的Future執行完成,await之後的程式碼才會繼續執行

Future<String> getNetworkData() async {
  var result = await Future.delayed(Duration(seconds: 3), () {
    return "network data";
  });

  return "請求到的資料:" + result;
}

test() async {
  var r = await getNetworkData();
  print(r);
}

void main() {
  print("main start");
  test();
  print("main end");
}
/*
印出
main start
main end
請求到的資料:network data
*/

如何建立 MicroTask

通過dartasynclibrary 下的scheduleMicrotask來建立一個MicroTask

import "dart:async";

main(List<String> args) {
  scheduleMicrotask(() {
    print("Hello MicroTask");
  });
  //印出 Hello MicroTask
}

如果我們有一個任務不希望它放在Event Queue中依次排隊,那麼就可以建立一個MicroTask

我們已經知道一個isolate有自己可以訪問的記憶體空間以及需要執行的事件迴圈,但是,如果只有一個isolate,那麼意味著我們只能永遠利用一個執行緒,這對於多核CPU來說,是一種資源的浪費,所以在開發中,如果有非常多耗時的計算,可以自己建立isolate,在獨立的isolate中完成想要的計算操作

isolate 中,資源隔離做得非常好,每個 isolate 都有自己的 Event Loop 與 Queue,isolate 之間不共享任何資源,只能依靠訊息機制通訊,因此也就沒有資源搶佔問題

如何建立 isolate

我們可以通過Isolate.spawn建立一個isolate

import "dart:isolate";

main() {
  Isolate.spawn(greet, "Hello Isolate");
}

void greet(info) {
  print("新的isolate:$info");
}

isolate 之間的通訊機制,我們需要新的Isolate進行計算,並且將計算結果告知Main Isolate (也就是預設開啟的Isolate),isolate 通過傳送管道(SendPort)實現訊息通訊機制,我們可以在新增isolate 時將Main Isolate的傳送管道作為引數傳遞給它,建立的 isolate 執行完畢時,可以利用這個管道給Main Isolate傳送資料

import "dart:isolate";

main(List<String> args) async {
  // 1.建立管道
  ReceivePort receivePort= ReceivePort();

  // 2.建立新的Isolate
  Isolate isolate = await Isolate.spawn<SendPort>(foo, receivePort.sendPort);

  // 3.監聽管道訊息
  receivePort.listen((data) {
    print('Data:$data');
    // 不再使用時,我們會關閉管道
    receivePort.close();
    // 需要將isolate釋放
    isolate?.kill(priority: Isolate.immediate);
  });
}

void foo(SendPort sendPort) {
  sendPort.send("Hello World");
}

但這只是單向通訊,要怎麼雙向通訊呢?

範例:

import 'dart:async';
import 'dart:isolate';

main() async {
  // isolate所需的參數,必須要有SendPort,SendPort需要ReceivePort来創建
  final receivePort = new ReceivePort();
  // 開始創建isolate, Isolate.spawn函數是isolate.dart裡的代碼,_isolate是我們自己實現的函數
  await Isolate.spawn(_isolate, receivePort.sendPort);
  // 發送的第一個訊息,是它的SendPort
  var sendPort = await receivePort.first;
  var msg = await sendReceive(sendPort, "foo");
  print('received $msg');
  msg = await sendReceive(sendPort, "bar");
  print('received $msg');
}

// 新isolate的入口函數
_isolate(SendPort replyTo) async {
  // 實例化一個ReceivePort 以接收消息
  var port = new ReceivePort();

  // 把它的sendPort發送給宿主isolate,以便宿主可以給他發送訊息
  replyTo.send(port.sendPort);

  // 監聽消息,從port裡取出
  await for (var msg in port) {
    var data = msg[0];
    SendPort replyTo = msg[1];
    replyTo.send('回答:' + data);
    if (data == "bar") port.close();
  }
}

// 對某個port發送消息,並接收結果
Future sendReceive(SendPort port, msg) {
  ReceivePort response = new ReceivePort();
  port.send([msg, response.sendPort]);
  return response.first;
}

應該什麼時候使用Futures 和 Isolate?

建議盡可能使用Future,因為一旦事件迴圈擁有空閒時間,這些Future就會被執行。讓使用者可以感覺事件正在被並行處理,如果確定事件需要繁重的處理以及一些時間才能完成,才考慮使用isolate

使用isolate例子:

網路請求:解碼JSON(HttpRequest的響應)可能需要一些時間 → 可以使用compute

compute:

適合一次性的運算,不需要與isolate 雙向溝通

主要功能有:產生一個isolate,在該isolate 運行一個回調函式,並傳遞一些數據,最後回調函式返回處理結果,回調執行完後釋放isolate。 (回調函式必須是頂級函數並且不能是閉包或類中的方法 ( 靜態或非靜態) )

參考連結1

參考連結2

參考連結3

總結

終於結束Dart 語言介紹的系列了,花了那麼多天,希望能把根基打穩,之後開發專案才能更清楚語法上的應用,接下來終於可以進入我們的Flutter 專案了!


上一篇
Day09 Dart 語言介紹(七) 泛型、Extension
下一篇
Day11 認識並設置 Android Studio 環境
系列文
從零開始的Flutter世界30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

1 則留言

0
whaaaaazup
iT邦新手 5 級 ‧ 2021-07-21 13:11:58

上網看那麼多篇dart非同步 這篇是講的最清楚的 推一個

我要留言

立即登入留言