編程中的代碼執行,一般分爲同步
與異步
兩種。簡單說,同步就是按照代碼的編寫順序,從上到下依次執行,這也是最簡單的咱們最常接觸的一種形式。可是同步代碼的缺點也顯而易見,若是其中某一行或幾行代碼很是耗時,那麼就會阻塞,使得後面的代碼不能被馬上執行。html
異步的出現正是爲了解決這種問題,它可使某部分耗時代碼不在當前這條執行線路上馬上執行,那究竟怎麼執行呢?最多見的一種方案是使用多線程,也就至關於開闢另外一條執行線,而後讓耗時代碼在另外一條執行線上運行,這樣兩條執行線並列,耗時代碼天然也就不能阻塞主執行線上的代碼了。git
多線程雖然好用,可是在大量併發時,仍然存在兩個較大的缺陷,一個是開闢線程比較耗費資源,線程開多了機器吃不消,另外一個則是線程的鎖問題,多個線程操做共享內存時須要加鎖,複雜狀況下的鎖競爭不只會下降性能,還可能形成死鎖。所以又出現了基於事件的異步模型。簡單說就是在某個單線程中存在一個事件循環和一個事件隊列,事件循環不斷的從事件隊列中取出事件來執行,這裏的事件就比如是一段代碼,每當遇到耗時的事件時,事件循環不會停下來等待結果,它會跳過耗時事件,繼續執行其後的事件。當不耗時的事件都完成了,再來查看耗時事件的結果。所以,耗時事件不會阻塞整個事件循環,這讓它後面的事件也會有機會獲得執行。github
咱們很容易發現,這種基於事件的異步模型,只適合I/O
密集型的耗時操做,由於I/O
耗時操做,每每是把時間浪費在等待對方傳送數據或者返回結果,所以這種異步模型每每用於網絡服務器併發。若是是計算密集型的操做,則應當儘量利用處理器的多核,實現並行計算。web
Dart 是事件驅動的體系結構,該結構基於具備單個事件循環和兩個隊列的單線程執行模型。 Dart雖然提供調用堆棧。 可是它使用事件在生產者和消費者之間傳輸上下文。 事件循環由單個線程支持,所以根本不須要同步和鎖定。編程
Dart 的兩個隊列分別是api
MicroTask queue
微任務隊列bash
Event queue
事件隊列服務器
Dart事件循環執行如上圖所示markdown
MicroTask
隊列是否爲空,不是則先執行MicroTask
隊列MicroTask
執行完後,檢查有沒有下一個MicroTask
,直到MicroTask
隊列爲空,纔去執行Event
隊列Evnet
隊列取出一個事件處理完後,再次返回第一步,去檢查MicroTask
隊列是否爲空咱們能夠看出,將任務加入到MicroTask
中能夠被儘快執行,但也須要注意,當事件循環在處理MicroTask
隊列時,Event
隊列會被卡住,應用程序沒法處理鼠標單擊、I/O消息等等事件。網絡
注意,如下調用的方法,都定義在dart:async
庫中。
將任務添加到MicroTask
隊列有兩種方法
import 'dart:async'; void myTask(){ print("this is my task"); } void main() { # 1. 使用 scheduleMicrotask 方法添加 scheduleMicrotask(myTask); # 2. 使用Future對象添加 new Future.microtask(myTask); } 複製代碼
將任務添加到Event
隊列
import 'dart:async'; void myTask(){ print("this is my task"); } void main() { 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類是對將來結果的一個代理,它返回的並非被調用的任務的返回值。
void myTask(){ print("this is my task"); } void main() { Future fut = new Future(myTask); } 複製代碼
如上代碼,Future
類實例fut
並非函數myTask
的返回值,它只是代理了myTask
函數,封裝了該任務的執行狀態。
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
複製代碼
當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
註冊的回調將被執行。
在Dart1.9中加入了async
和await
關鍵字,有了這兩個關鍵字,咱們能夠更簡潔的編寫異步代碼,而不須要調用Future
相關的API
將 async
關鍵字做爲方法聲明的後綴時,具備以下意義
Future
對象做爲返回值// 導入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
的使用。
前面已經說過,將很是耗時的任務添加到事件隊列後,仍然會拖慢整個事件循環的處理,甚至是阻塞。可見基於事件循環的異步模型仍然是有很大缺點的,這時候咱們就須要Isolate
,這個單詞的中文意思是隔離。
簡單說,能夠把它理解爲Dart中的線程。但它又不一樣於線程,更恰當的說應該是微線程,或者說是協程。它與線程最大的區別就是不能共享內存,所以也不存在鎖競爭問題,兩個Isolate
徹底是兩條獨立的執行線,且每一個Isolate
都有本身的事件循環,它們之間只能經過發送消息通訊,因此它的資源開銷低於線程。
從主Isolate
建立一個新的Isolate
有兩種方法
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
對象,第一個元素是整型,表示消息類型,第二個元素則表示消息內容。
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
中的任務完成了,它也不會馬上退出,所以,當使用完本身建立的Isolate
後,最好調用newIsolate.kill(priority: Isolate.immediate);
將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 解碼、加密、圖像處理:好比剪裁、長時間的網絡請求來加載資源
參考資料: Dart 文檔 Isolate 文檔
博主發佈的相關視頻課程
歡迎關注個人公衆號:編程之路從0到1