一,概述
編程中的代碼執行,通常分為同步與異步兩種。
- 同步:
簡單說,同步就是按照代碼的編寫順序,從上到下依次執行,這也是最簡單的我們最常接觸的一種形式。但是同步代碼的缺點也顯而易見,如果其中某一行或幾行代碼非常耗時,那么就會阻塞,使得后面的代碼不能被立刻執行。 - 異步:
異步的出現正是為了解決這種問題,它可以使某部分耗時代碼不在當前這條執行線路上立刻執行,那究竟怎么執行呢?最常見的一種方案是使用多線程,也就相當於開辟另一條執行線,然后讓耗時代碼在另一條執行線上運行,這樣兩條執行線並列,耗時代碼自然也就不能阻塞主執行線上的代碼了。
多線程雖然好用,但是在大量並發時,仍然存在兩個較大的缺陷,一個是開辟線程比較耗費資源,線程開多了機器吃不消,另一個則是線程的鎖問題,多個線程操作共享內存時需要加鎖,復雜情況下的鎖競爭不僅會降低性能,還可能造成死鎖。因此又出現了基於事件的異步模型。
異步模型簡單說就是在某個單線程中存在一個事件循環和一個事件隊列,事件循環不斷的從事件隊列中取出事件來執行,這里的事件就好比是一段代碼,每當遇到耗時的事件時,事件循環不會停下來等待結果,它會跳過耗時事件,繼續執行其后的事件。當不耗時的事件都完成了,再來查看耗時事件的結果。因此,耗時事件不會阻塞整個事件循環,這讓它后面的事件也會有機會得到執行。
我們很容易發現,這種基於事件的異步模型,只適合I/O密集型的耗時操作,因為I/O耗時操作,往往是把時間浪費在等待對方傳送數據或者返回結果,因此這種異步模型往往用於網絡服務器並發。如果是計算密集型的操作,則應當盡可能利用處理器的多核,實現並行計算。
二,Dart 的事件循環
Dart 是事件驅動的體系結構,該結構基於具有單個事件循環和兩個隊列的單線程執行模型。 Dart雖然提供調用堆棧。 但是它使用事件在生產者和消費者之間傳輸上下文。 事件循環由單個線程支持,因此根本不需要同步和鎖定。
Dart 的兩個隊列分別是
-
MicroTask queue
微任務隊列 -
Event queue
事件隊列
Dart事件循環執行如上圖所示
- 先查看MicroTask隊列是否為空,不是則先執行MicroTask隊列
- 一個MicroTask執行完后,檢查有沒有下一個MicroTask,直到MicroTask隊列為空,才去執行Event隊列
- 在Evnet 隊列取出一個事件處理完后,再次返回第一步,去檢查MicroTask隊列是否為空
注意:我們可以看出,將任務加入到MicroTask
中可以被盡快執行,但也需要注意,當事件循環在處理MicroTask
隊列時,Event
隊列會被卡住,應用程序無法處理鼠標單擊、I/O消息等等事件。
三,調度任務
注意: 以下調用的方法,都定義在dart: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);}
- 使用Future對象添加
- 編寫代碼驗證以上的結論
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中加入了async和await關鍵字,有了這兩個關鍵字,我們可以更簡潔的編寫異步代碼,而不需要調用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
中
並發可以使用用
isolate
,
isolate
和
Thread
很像,但是
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 解碼
- 加密
- 圖像處理:比如剪裁
- 網絡請求:加載資源、圖片
九,參考: