Flutter中的非同步

QiShare發表於2021-07-22

同步與非同步

程式的執行是出於滿足人們對某種邏輯需求的處理,在計算機上表現為可執行指令,正常情況下我們期望的指令是按邏輯的順序依次執行的,而實際情況由於某些指令是耗時操作,不能立即返回結果而造成了阻塞,導致程式無法繼續執行。這種情況多見於一些io操作。這時,對於使用者層面來說,我們可以選擇stop the world,等待操作完成返回結果後再繼續操作,也可以選擇繼續去執行其他操作,等事件返回結果後再通知回來。這就是從使用者角度來看的同步與非同步。

從作業系統的角度,同步非同步,與任務排程,程式間切換,中斷,系統呼叫之間有著更為複雜的關係。

同步I/O 與 非同步I/O的區別

img

為什麼使用非同步

使用者可以阻塞式的等待,因為人的操作和計算機相比是非常慢的,計算機如果阻塞那就是很大的效能浪費了,非同步操作讓您的程式在等待另一個操作的同時完成工作。三種非同步操作的場景:

  • I/O操作:例如:發起一個網路請求,讀寫資料庫、讀寫檔案、列印文件等,一個同步的程式去執行這些操作,將導致程式的停止,直到操作完成。更有效的程式會改為在操作掛起時去執行其他操作,假設您有一個程式讀取一些使用者輸入,進行一些計算,然後通過電子郵件傳送結果。傳送電子郵件時,您必須向網路傳送一些資料,然後等待接收伺服器響應。等待伺服器響應所投入的時間是浪費的時間,如果程式繼續計算,這將得到更好的利用
  • 並行執行多個操作:當您需要並行執行不同的操作時,例如進行資料庫呼叫、Web 服務呼叫以及任何計算,那麼我們可以使用非同步
  • 長時間執行的基於事件驅動的請求:這就是您有一個請求進來的想法,並且該請求進入休眠狀態一段時間等待其他一些事件的發生。當該事件發生時,您希望請求繼續,然後向客戶端傳送響應。所以在這種情況下,當請求進來時,執行緒被分配給該請求,當請求進入睡眠狀態時,執行緒被髮送回執行緒池,當任務完成時,它生成事件並從執行緒池中選擇一個執行緒傳送響應

計算機中非同步的實現方式就是任務排程,也就是程式的切換

任務排程採用的是時間片輪轉的搶佔式排程方式,程式是任務排程的最小單位。

計算機系統分為使用者空間核心空間,使用者程式在使用者空間,作業系統執行在核心空間,核心空間的資料訪問修改擁有高於普通程式的許可權,使用者程式之間相互獨立,記憶體不共享,保證作業系統的執行安全。如何最大化的利用CPU,確定某一時刻哪個程式擁有CPU資源就是任務排程的過程。核心負責排程管理使用者程式,以下為程式排程過程

img

在任意時刻, 一個 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 將返回firstFuturesecondFuture中第一個返回結果的值

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"

img

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等事件,所以要謹慎使用,如下為流程圖:

event queue和microtask queue

這兩個任務佇列中的任務切換在某些方面就相當於是協程排程機制

協程

協程是一種協作式的任務排程機制,區別於作業系統的搶佔式任務排程機制,它是使用者態下面的,避免執行緒切換的核心態、使用者態轉換的效能開銷。它讓呼叫者自己來決定什麼時候讓出cpu,比作業系統的搶佔式排程所需要的時間代價要小很多,後者為了恢復現場會儲存相當多的狀態(不僅包括程式上下文的虛擬記憶體、棧、全域性變數等使用者空間的資源,還包括了核心堆疊、暫存器等核心空間的狀態),並且會頻繁的切換,以現在流行的大多數Linux機器來說,每一次的上下文切換要消耗大約1.2-1.5μs的時間,這是僅考慮直接成本,固定在單個核心以避免遷移的成本,未固定情況下,切換時間可達2.2μs

img

對cpu來說這算一個很長的時間嗎,一個很好的比較是memcpy,在相同的機器上,完成一個64KiB資料的拷貝需要3μs的時間,上下文的切換比這個操作稍微快一些

Plot of thread/process launch and context switch

協程和執行緒非常相似,是從非同步執行任務的角度來看,而並不是從設計的實體角度像程式->執行緒->協程這樣類似於細胞->原子核->質子中子這樣的關係。可以理解為執行緒上執行的一段函式,用yield完成非同步請求、註冊回撥/通知器、儲存狀態,掛起控制流、收到回撥/通知、恢復狀態、恢復控制流的所有過程

多執行緒執行任務模型如圖:

執行緒的阻塞要靠系統間程式的切換,完成邏輯流的執行,頻繁的切換耗費大量資源,而且邏輯流的執行數量嚴重依賴於程式申請到的執行緒的數量。

協程是協同多工的,這意味著協程提供併發性但不提供並行性,執行流模型圖如下:

協程可以用邏輯流的順序去寫控制流,協程的等待會主動釋放cpu,避免了執行緒切換之間的等待時間,有更好的效能,邏輯流的程式碼編寫和理解上也簡單的很多

但是執行緒並不是一無是處,搶佔式執行緒排程器事實上提供了準實時的體驗。例如Timer,雖然不能確保在時間到達的時候一定能夠分到時間片執行,但不會像協程一樣萬一沒有人讓出時間片就永遠得不到執行……

總結

  • 同步與非同步
  • Future提供了Flutter中非同步程式碼鏈式編寫方式
  • async-wait提供了非同步程式碼的同步書寫方式
  • Future的常用方法和FutureBuilder編寫UI
  • Flutter中執行緒模型,四個執行緒
  • 單執行緒語言的事件驅動模型
  • 程式間切換和協程對比

參考

dart.cn/tutorials/l…

dart.cn/codelabs/as…

medium.com/dartlang/da…

juejin.cn/post/684490…

developer.mozilla.org/en-US/docs/…

www.zhihu.com/question/19…

www.zhihu.com/question/50…

en.wikipedia.org/wiki/Asynch…

eli.thegreenplace.net/2018/measur…

相關文章