同步與非同步
程式的執行是出於滿足人們對某種邏輯需求的處理,在計算機上表現為可執行指令,正常情況下我們期望的指令是按邏輯的順序依次執行的,而實際情況由於某些指令是耗時操作,不能立即返回結果而造成了阻塞,導致程式無法繼續執行。這種情況多見於一些io操作。這時,對於使用者層面來說,我們可以選擇stop the world,等待操作完成返回結果後再繼續操作,也可以選擇繼續去執行其他操作,等事件返回結果後再通知回來。這就是從使用者角度來看的同步與非同步。
從作業系統的角度,同步非同步,與任務排程,程式間切換,中斷,系統呼叫之間有著更為複雜的關係。
同步I/O 與 非同步I/O的區別
為什麼使用非同步
使用者可以阻塞式的等待,因為人的操作和計算機相比是非常慢的,計算機如果阻塞那就是很大的效能浪費了,非同步操作讓您的程式在等待另一個操作的同時完成工作。三種非同步操作的場景:
- I/O操作:例如:發起一個網路請求,讀寫資料庫、讀寫檔案、列印文件等,一個同步的程式去執行這些操作,將導致程式的停止,直到操作完成。更有效的程式會改為在操作掛起時去執行其他操作,假設您有一個程式讀取一些使用者輸入,進行一些計算,然後通過電子郵件傳送結果。傳送電子郵件時,您必須向網路傳送一些資料,然後等待接收伺服器響應。等待伺服器響應所投入的時間是浪費的時間,如果程式繼續計算,這將得到更好的利用
- 並行執行多個操作:當您需要並行執行不同的操作時,例如進行資料庫呼叫、Web 服務呼叫以及任何計算,那麼我們可以使用非同步
- 長時間執行的基於事件驅動的請求:這就是您有一個請求進來的想法,並且該請求進入休眠狀態一段時間等待其他一些事件的發生。當該事件發生時,您希望請求繼續,然後向客戶端傳送響應。所以在這種情況下,當請求進來時,執行緒被分配給該請求,當請求進入睡眠狀態時,執行緒被髮送回執行緒池,當任務完成時,它生成事件並從執行緒池中選擇一個執行緒傳送響應
計算機中非同步的實現方式就是任務排程,也就是程式的切換
任務排程採用的是時間片輪轉的搶佔式排程方式,程式是任務排程的最小單位。
計算機系統分為使用者空間
和核心空間
,使用者程式在使用者空間,作業系統執行在核心空間,核心空間的資料訪問修改擁有高於普通程式的許可權,使用者程式之間相互獨立,記憶體不共享,保證作業系統的執行安全。如何最大化的利用CPU,確定某一時刻哪個程式擁有CPU資源就是任務排程的過程。核心負責排程管理使用者程式,以下為程式排程過程
在任意時刻, 一個 CPU 核心上(processor)只可能執行一個程式
每一個程式可以包含多個執行緒,執行緒是執行操作的最小單元,因此程式的切換落實到具體細節就是正在執行執行緒的切換
Future
Future<T> 表示一個非同步的操作結果,用來表示一個延遲的計算,返回一個結果或者error
,使用程式碼例項:
Future<int> future = getFuture();
future.then((value) => handleValue(value))
.catchError((error) => handleError(error))
.whenComplete(func);
複製程式碼
future可以是三種狀態:未完成的
、返回結果值
、返回異常
當一個返回future物件被呼叫時,會發生兩件事:
- 將函式操作入佇列等待執行結果並返回一個未完成的Future物件
- 函式操作完成時,
Future
物件變為完成並攜帶一個值或一個錯誤
首先,Flutter事件處理模型為先執行main函式,完成後檢查執行微任務佇列Microtask Queue
中事件,最後執行事件佇列Event Queue
中的事件,示例:
void main(){
Future(() => print(10));
Future.microtask(() => print(9));
print("main");
}
/// 列印結果為:
/// main
/// 9
/// 10
複製程式碼
基於以上事件模型的基礎上,看下Future提供的幾種建構函式,其中最基本的為直接傳入一個Function
:
factory Future(FutureOr<T> computation()) {
_Future<T> result = new _Future<T>();
Timer.run(() {
try {
result._complete(computation());
} catch (e, s) {
_completeWithErrorCallback(result, e, s);
}
});
return result;
}
複製程式碼
Function
有多種寫法:
//簡單操作,單步
Future(() => print(5));
//稍複雜,匿名函式
Future((){
print(6);
});
//更多操作,方法名
Future(printSeven);
printSeven(){
print(7);
}
複製程式碼
Future.microtask
此工程方法建立的事件將傳送到微任務佇列Microtask Queue
,具有相比事件佇列Event Queue
優先執行的特點
factory Future.microtask(FutureOr<T> computation()) {
_Future<T> result = new _Future<T>();
//
scheduleMicrotask(() {
try {
result._complete(computation());
} catch (e, s) {
_completeWithErrorCallback(result, e, s);
}
});
return result;
}
複製程式碼
Future.sync
返回一個立即執行傳入引數的Future,可理解為同步呼叫
factory Future.sync(FutureOr<T> computation()) {
try {
var result = computation();
if (result is Future<T>) {
return result;
} else {
// TODO(40014): Remove cast when type promotion works.
return new _Future<T>.value(result as dynamic);
}
} catch (error, stackTrace) {
/// ...
}
}
複製程式碼
Future.microtask(() => print(9));
Future(() => print(10));
Future.sync(() => print(11));
/// 列印結果: 11、9、10
複製程式碼
Future.value
建立一個將來包含value的future
factory Future.value([FutureOr<T>? value]) {
return new _Future<T>.immediate(value == null ? value as T : value);
}
複製程式碼
引數FutureOr含義為T value 和 Future value 的合集,因為對於一個Future引數來說,他的結果可能為value或者是Future,所以對於以下兩種寫法均合法:
Future.value(12).then((value) => print(value));
Future.value(Future<int>((){
return 13;
}));
複製程式碼
這裡需要注意即使value接收的是12,仍然會將事件傳送到Event佇列等待執行,但是相對其他Future事件執行順序會提前
Future.error
建立一個執行結果為error的future
factory Future.error(Object error, [StackTrace? stackTrace]) {
/// ...
return new _Future<T>.immediateError(error, stackTrace);
}
_Future.immediateError(var error, StackTrace stackTrace)
: _zone = Zone._current {
_asyncCompleteError(error, stackTrace);
}
複製程式碼
Future.error(new Exception("err msg"))
.then((value) => print("err value: $value"))
.catchError((e) => print(e));
/// 執行結果為:Exception: err msg
複製程式碼
Future.delayed
建立一個延遲執行回撥的future,內部實現為Timer加延時執行一個Future
factory Future.delayed(Duration duration, [FutureOr<T> computation()?]) {
/// ...
new Timer(duration, () {
if (computation == null) {
result._complete(null as T);
} else {
try {
result._complete(computation());
} catch (e, s) {
_completeWithErrorCallback(result, e, s);
}
}
});
return result;
}
複製程式碼
Future.wait
等待多個Future並收集返回結果
static Future<List<T>> wait<T>(Iterable<Future<T>> futures,
{bool eagerError = false, void cleanUp(T successValue)?}) {
/// ...
}
複製程式碼
FutureBuilder結合使用:
child: FutureBuilder(
future: Future.wait([
firstFuture(),
secondFuture()
]),
builder: (context,snapshot){
if(!snapshot.hasData){
return CircularProgressIndicator();
}
final first = snapshot.data[0];
final second = snapshot.data[1];
return Text("data $first $second");
},
),
複製程式碼
Future.any
返回futures集合中第一個返回結果的值
static Future<T> any<T>(Iterable<Future<T>> futures) {
var completer = new Completer<T>.sync();
void onValue(T value) {
if (!completer.isCompleted) completer.complete(value);
}
void onError(Object error, StackTrace stack) {
if (!completer.isCompleted) completer.completeError(error, stack);
}
for (var future in futures) {
future.then(onValue, onError: onError);
}
return completer.future;
}
複製程式碼
對上述例子來說,Future.any
snapshot.data
將返回firstFuture
、secondFuture
中第一個返回結果的值
Future.forEach
為傳入的每一個元素,順序執行一個action
static Future forEach<T>(Iterable<T> elements, FutureOr action(T element)) {
var iterator = elements.iterator;
return doWhile(() {
if (!iterator.moveNext()) return false;
var result = action(iterator.current);
if (result is Future) return result.then(_kTrue);
return true;
});
}
複製程式碼
這裡邊action是方法作為引數,頭一次見這種形式語法還是在js中,當時就迷惑了很大一會兒,使用示例:
Future.forEach(["one","two","three"], (element) {
print(element);
});
複製程式碼
Future.doWhile
執行一個操作直到返回false
Future.doWhile((){
for(var i=0;i<5;i++){
print("i => $i");
if(i >= 3){
return false;
}
}
return true;
});
/// 結果列印到 3
複製程式碼
以上為Future中常用建構函式和方法
在Widget中使用Future
Flutter提供了配合Future顯示的元件FutureBuilder
,使用也很簡單,虛擬碼如下:
child: FutureBuilder(
future: getFuture(),
builder: (context, snapshot){
if(!snapshot.hasData){
return CircularProgressIndicator();
} else if(snapshot.hasError){
return _ErrorWidget("Error: ${snapshot.error}");
} else {
return _ContentWidget("Result: ${snapshot.data}")
}
}
)
複製程式碼
Async-await
使用
這兩個關鍵字提供了非同步方法的同步書寫方式,Future提供了方便的鏈式呼叫使用方式,但是不太直觀,而且大量的回撥巢狀造成可閱讀性差。因此,現在很多語言都引入了await-async語法,學習他們的使用方式是很有必要的。
兩條基本原則:
- 定義一個非同步方法,必須在方法體前宣告 async
- await關鍵字必須在async方法中使用
首先,在要執行耗時操作的方法體前增加async:
void main() async { ··· }
複製程式碼
然後,根據方法的返回型別新增Future修飾
Future<void> main() async { ··· }
複製程式碼
現在就可以使用await關鍵字來等待這個future執行完畢
print(await createOrderMessage());
複製程式碼
例如實現一個由一級分類獲取二級分類,二級分類獲取詳情的需求,使用鏈式呼叫的程式碼如下:
var list = getCategoryList();
list.then((value) => value[0].getCategorySubList(value[0].id))
.then((subCategoryList){
var courseList = subCategoryList[0].getCourseListByCategoryId(subCategoryList[0].id);
print(courseList);
}).catchError((e) => (){
print(e);
});
複製程式碼
現在來看下使用async/await,事情變得簡單了多少
Future<void> main() async {
await getCourses().catchError((e){
print(e);
});
}
Future<void> getCourses() async {
var list = await getCategoryList();
var subCategoryList = await list[0].getCategorySubList(list[0].id);
var courseList = subCategoryList[0].getCourseListByCategoryId(subCategoryList[0].id);
print(courseList);
}
複製程式碼
可以看到這樣更加直觀
缺陷
async/await 非常方便,但是還是有一些缺點需要注意
因為它的程式碼看起來是同步的,所以是會阻塞後面的程式碼執行,直到await返回結果,就像執行同步操作一樣。它確實可以允許其他任務在此期間繼續執行,但後邊自己的程式碼被阻塞。
這意味著程式碼可能會由於有大量await程式碼相繼執行而阻塞,本來用Future編寫表示並行的操作,現在使用await變成了序列,例如,首頁有一個同時獲取輪播介面,tab列表介面,msg列表介面的需求
Future<String> getBannerList() async {
return await Future.delayed(Duration(seconds: 3),(){
return "banner list";
});
}
Future<String> getHomeTabList() async {
return await Future.delayed(Duration(seconds: 3),(){
return "tab list";
});
}
Future<String> getHomeMsgList() async {
return await Future.delayed(Duration(seconds: 3),(){
return "msg list";
});
}
複製程式碼
使用await編寫很可能會寫成這樣,列印執行操作的時間
Future<void> main2() async {
var startTime = DateTime.now().second;
await getBannerList();
await getHomeTabList();
await getHomeMsgList();
var endTime = DateTime.now().second;
print(endTime - startTime); // 9
}
複製程式碼
在這裡,我們直接等待所有三個模擬介面的呼叫,使每個呼叫3s。後續的每一個都被迫等到上一個完成, 最後會看到總執行時間為9s,而實際我們想三個請求同時執行,程式碼可以改成如下這種:
Future<void> main() async {
var startTime = DateTime.now().second;
var bannerList = getBannerList();
var homeTabList = getHomeTabList();
var homeMsgList = getHomeMsgList();
await bannerList;
await homeTabList;
await homeMsgList;
var endTime = DateTime.now().second;
print(endTime - startTime); // 3
}
複製程式碼
將三個Future儲存在變數中,這樣可以同時啟動,最後列印時間僅為3s,所以在編寫程式碼時,我們必須牢記這點,避免效能損耗。
原理
執行緒模型
當一個Flutter應用或者Flutter Engine啟動時,它會啟動(或者從池中選擇)另外三個執行緒,這些執行緒有些時候會有重合的工作點,但是通常,它們被稱為UI執行緒
,GPU執行緒
,IO執行緒
。需要注意一點這個UI執行緒並不是程式執行的主執行緒,或者說和其他平臺上的主執行緒理解不同,通常的,Flutter將平臺的主執行緒叫做"Platform thread"
UI執行緒是所有的Dard程式碼執行的地方,例如framework和你的應用,除非你啟動自己的isolates,否則Dart將永遠不會執行在其他執行緒。平臺執行緒是所有依賴外掛的程式碼執行的地方。該執行緒也是native frameworks
為其他任務提供服務的地方,一般來說,一個Flutter應用啟動的時候會建立一個Engine例項,Engine建立的時候會建立一個Platform thread為其提供服務。跟Flutter Engine的所有互動(介面呼叫)必須發生在Platform Thread,試圖在其它執行緒中呼叫Flutter Engine會導致無法預期的異常。這跟Android/iOS UI相關的操作都必須在主執行緒進行相類似。
Isolates是Dart中概念,本意是隔離,它的實現功能和thread類似,但是他們之間的實現又有著本質的區別,Isolote是獨立的工作者,它們之間不共享記憶體,而是通過channel傳遞訊息。Dart是單執行緒執行程式碼,Isolate提供了Dart應用可以更好的利用多核硬體的解決方案。
事件迴圈
單執行緒模型中主要就是在維護著一個事件迴圈(Event Loop) 與 兩個佇列(event queue和microtask queue)當Flutter專案程式觸發如點選事件
、IO事件
、網路事件時
,它們就會被加入到eventLoop中,eventLoop一直在迴圈之中,當主執行緒發現事件佇列不為空時發現,就會取出事件,並且執行。
microtask queue中事件優先於event queue執行,當有任務傳送到microtask佇列時,會在當前event執行完成後,阻塞當前event queue轉而去執行microtask queue中的事件,這樣為Dart提供了任務插隊的解決方案。
event queue的阻塞意味著app無法進行UI繪製,響應滑鼠和I/O等事件,所以要謹慎使用,如下為流程圖:
這兩個任務佇列中的任務切換在某些方面就相當於是協程排程機制
協程
協程是一種協作式的任務排程機制,區別於作業系統的搶佔式任務排程機制,它是使用者態下面的,避免執行緒切換的核心態、使用者態轉換的效能開銷。它讓呼叫者自己來決定什麼時候讓出cpu,比作業系統的搶佔式排程所需要的時間代價要小很多,後者為了恢復現場會儲存相當多的狀態(不僅包括程式上下文的虛擬記憶體、棧、全域性變數等使用者空間的資源,還包括了核心堆疊、暫存器等核心空間的狀態),並且會頻繁的切換,以現在流行的大多數Linux機器來說,每一次的上下文切換要消耗大約1.2-1.5μs的時間,這是僅考慮直接成本,固定在單個核心以避免遷移的成本,未固定情況下,切換時間可達2.2μs
對cpu來說這算一個很長的時間嗎,一個很好的比較是memcpy
,在相同的機器上,完成一個64KiB資料的拷貝需要3μs的時間,上下文的切換比這個操作稍微快一些
協程和執行緒非常相似,是從非同步執行任務的角度來看,而並不是從設計的實體角度像程式->執行緒->協程這樣類似於細胞->原子核->質子中子這樣的關係。可以理解為執行緒上執行的一段函式,用yield完成非同步請求、註冊回撥/通知器、儲存狀態,掛起控制流、收到回撥/通知、恢復狀態、恢復控制流的所有過程
多執行緒執行任務模型如圖:
執行緒的阻塞要靠系統間程式的切換,完成邏輯流的執行,頻繁的切換耗費大量資源,而且邏輯流的執行數量嚴重依賴於程式申請到的執行緒的數量。
協程是協同多工的,這意味著協程提供併發性但不提供並行性,執行流模型圖如下:
協程可以用邏輯流的順序去寫控制流,協程的等待會主動釋放cpu,避免了執行緒切換之間的等待時間,有更好的效能,邏輯流的程式碼編寫和理解上也簡單的很多
但是執行緒並不是一無是處,搶佔式執行緒排程器事實上提供了準實時的體驗。例如Timer,雖然不能確保在時間到達的時候一定能夠分到時間片執行,但不會像協程一樣萬一沒有人讓出時間片就永遠得不到執行……
總結
- 同步與非同步
- Future提供了Flutter中非同步程式碼鏈式編寫方式
- async-wait提供了非同步程式碼的同步書寫方式
- Future的常用方法和FutureBuilder編寫UI
- Flutter中執行緒模型,四個執行緒
- 單執行緒語言的事件驅動模型
- 程式間切換和協程對比