Flutter?異步編程之單線程下異步模型圖文示例詳解
一、 本專欄圖示概念規(guī)范
本專欄是對 異步編程 的系統(tǒng)探索,會通過各個方面去認知、思考 異步編程 的概念。期間會用到一些圖片進行表達與示意,在一開始先對圖中的元素和 基本概念 進行規(guī)范和說明。
1. 任務概念規(guī)范
任務 : 完成一項需求的基本單位。
分發(fā)任務: 觸發(fā)任務開始的動作。
任務結(jié)束: 任務完成的標識。
任務生命期: 任務從開始到完成的時間跨度。
如下所示,方塊 表示任務;當 箭頭指向一個任務時,表示對該任務進行分發(fā);任何被分發(fā)的任務都會結(jié)束。在任務分發(fā)和結(jié)束之間,有一條虛線進行連接,表示 任務生命期 。

2. 任務的狀態(tài)
未完成 : Uncompleted
成功完成 : Completed with Success
異常結(jié)束 : Completed with Error
一個任務生命期間有三種狀態(tài),如下通過三種顏色表示。在 任務結(jié)束 之前,該任務都是 未完成 態(tài),通過 淺藍色 表示;任何被分發(fā)的任務都是為了完成某項需求,任何任務都會結(jié)束,在結(jié)束時刻根據(jù)是否完成需求,可分為 成功完成 和 異常結(jié)束 兩種狀態(tài),如下分別用 綠色 和 紅色 表示。

3. 時刻與時間線
機體 : 任務分發(fā)者或處理者。
時刻: 機體運行中的某一瞬間。
時間線: 所有時刻構(gòu)成的連續(xù)有向軸線。
在一個機體運行的過程中,時間線是絕對的,通過 紫色有向線段 表示時間的流逝的方向。時刻 是時間線上任意一點 ,通過 黑點 表示。

4.同步與異步
同步 : 機體在時間線上,將任務按順序依次分發(fā)。
同步執(zhí)行任務時,前一個任務完成后,才會分發(fā)下一任務。意思是說: 任意時刻只有一個任務在生命期中。

異步: 機體在時間線上,在一個任務未完成時,分發(fā)另一任務。
也就是說通過異步編程,允許某時刻有兩個及以上的任務在生命期中。如下所示,在 任務1 完成后,分發(fā) 任務2; 在 任務2 未結(jié)束的情況下,可以分發(fā) 任務 3 。此時對于任務 3 來說,任務 2 就是異步執(zhí)行的。

二、理解單線程中的異步任務
上面對基本概念進行了規(guī)范,看起來可能比較抽象,下面我們通過一個小場景來理解一下。媽媽早上出門散步,臨走前囑咐:
小捷,別睡了??炱鸫?,把被子曬一下,地掃一下。還有,沒開水了,記得燒。
當前場景下只有小捷 一個機體,需要完成的任務有四個:起床、曬被、拖地 、燒水 。

1. 任務的分配
當機體有多個任務需要分發(fā)時,需要對任務進行分配。認識任務之間的關(guān)系,是任務分配的第一步。只有理清關(guān)系,才能合理分配任務。分配過程中需要注意:
[1] 任務之間可能存在明確的先后順序,比如起床 需要在 曬被 之前。
[2] 任務之間先后順序也可能無所謂,比如先掃地還是先曬被,并沒有太大區(qū)別。
[3] 某類任務只需要機體來分發(fā),生命期中不需要機體處理,并且和后續(xù)的任務沒有什么關(guān)聯(lián)性,比如燒水任務。

像燒水這種任務,即耗時,又不需要機體在任務生命期中做什么事。如果這類任務使用同步處理,那么任務期間機體能做的事只有 等待 。對于一個機體來說,這種等待就會意味著阻塞,不能處理任何事。
結(jié)合日常生活,我們知道當前場景之中,想要發(fā)揮機體最大的效力,最好的方式是起床之后,先分發(fā) 燒水任務,不需要等待燒水任務完成,就去執(zhí)行曬被、掃地任務。這樣的任務分配就是將 燒水 作為一個異步任務來執(zhí)行的。
但在如果在分配時,將燒水作為最后一個任務,那么異步執(zhí)行的價值就會消失。所以對任務的合理分配,對機體的處理效率是非常重要的。
2.異步任務特點
從上面可以看出,異步任務 有很明顯的特征,并不是任何任務都有必要異步執(zhí)行。特別是對于單一機體來說,任務生命期間需要機體親自參與,是無法異步處理的。 比如一個人不能一邊曬被 ,一邊 掃地 。所以對于單線程來說,像一些只需要 分發(fā)任務,任務的具體執(zhí)行邏輯由其他機體完成的任務,適合使用 異步 處理,來避免不必要的等待。
這種任務,在應用程序中最常見的是網(wǎng)絡 io和 磁盤 io 的任務。比如,從一個網(wǎng)絡接口中獲取數(shù)據(jù),對于機體來說,只需要分發(fā)任務來發(fā)送請求,就像燒水時只需要裝水按下啟動鍵一樣。而服務器如何根據(jù)請求,查詢數(shù)據(jù)庫來返回響應信息,數(shù)據(jù)如何在網(wǎng)絡中傳輸?shù)?,和分發(fā)任務的機體沒有關(guān)系。磁盤的訪問也是一樣,分發(fā)讀寫文件任務后,真正干活的是操作系統(tǒng)。
像這類任務通過異步處理,可以避免在分發(fā)任務后,機體因等待任務的結(jié)束而阻塞。在等待其他機體處理的過程中,去分發(fā)其他任務,可以更好地分配時間。比如下面所示,網(wǎng)絡數(shù)據(jù)獲取 的任務分發(fā)后,需要通過網(wǎng)絡把請求傳輸給服務器,服務器進行處理,給出響應結(jié)果。

整個任務處理的過程,并不需要機體參與,所以分發(fā) 網(wǎng)絡數(shù)據(jù)獲取 任務后,無需等待任務完成,接著分發(fā) 構(gòu)建加載中界面 的任務,來展示加載中的界面。從而給出用戶交互的反饋,而不是阻塞在那里等待網(wǎng)絡任務完成,這就是一個非常典型的異步任務使用場景。
3. 異步任務完成與回調(diào)
前面的介紹中可以看出,異步任務在分發(fā)之后,并不會等待任務完成,在任務生命期中,可以繼續(xù)分發(fā)其他任務。但任何任務都會結(jié)束,很多時候我們需要知道異步任務何時完成,以及任務的完成情況、任務返回的結(jié)果,以便該任務后續(xù)的處理。比如,在燒水完成之后,我們需要處理 沖水 的任務。

這就要涉及到一個對異步而言非常重要的概念:
回調(diào): 任務在生命期間向機體提供通知的方式。
比如 燒水 任務完成后,燒水壺 “叮” 的一聲通知任務完成;或者燒水期間發(fā)生故障,發(fā)出報警提示。這種在任務生命期間向機體發(fā)送通知的方式稱為回調(diào) 。在編程中,回調(diào)一般是通過 函數(shù)參數(shù) 來實現(xiàn)的,所以習慣稱 回調(diào)函數(shù) 。 另外,函數(shù)可以傳遞數(shù)據(jù),所以通過回調(diào)函數(shù)不僅可以知道任務結(jié)束的契機,還可以通過回調(diào)參數(shù)將任務的內(nèi)部數(shù)據(jù)暴露給機體。
比如在實際開發(fā)中,分發(fā) 網(wǎng)絡數(shù)據(jù)獲取 的任務,其目的是為了通過網(wǎng)絡接口獲取數(shù)據(jù)。就像燒開水任務完成之后,需要把 開水 倒入瓶中一樣。我們也需要知道 網(wǎng)絡數(shù)據(jù)獲取 的任務完成的時機,將獲取的數(shù)據(jù) "倒入" 界面中進行顯示。

從發(fā)送異步任務,到異步任務結(jié)束的回調(diào)觸發(fā),就是一個異步任務完整的 生命期。
三、 Dart 語言中的異步
上面只是介紹了 異步模型 中的概念,這些概念是共通的,無論什么編程語言都一樣適用。就像現(xiàn)實中,無論使用哪國的語言表述,四則運算的概念都不會有任何區(qū)別。只是在表述過程中,表現(xiàn)形式會在語言的語法上有所差異。
1.編程語言中與異步模型的對應關(guān)系
每種語言的描述,都是對概念模型的具象化實現(xiàn)。這里既然是對 Flutter 中異步編程的介紹,自然要說一下 Dart 語言對異步模型的描述。
對于 任務 概念來說,在編程中和 函數(shù) 有著千絲萬縷的聯(lián)系:函數(shù)體 可以實現(xiàn) 任務處理的具體邏輯,也可以觸發(fā) 任務分發(fā)的動作 。但我并不認為兩者是等價的, 任務 有著明確的 目的性 ,而 函數(shù) 是實現(xiàn)這種 目的 的手段。在編程活動中,函數(shù) 作為 任務 在代碼中的邏輯體現(xiàn),任務 應先于 函數(shù) 存在。
如下代碼所示,在 main 函數(shù)中,觸發(fā) calculate 任務,計算 0 ~ count 累加值和計算耗時,并返回。其中 calculate 函數(shù)就是對該任務的代碼實現(xiàn):
void main(){
TaskResult result = calculate();
}
TaskResult calculate({int count = 10000000}){
int startTime = DateTime.now().millisecondsSinceEpoch;
int result = loopAdd(count);
int cost = DateTime.now().millisecondsSinceEpoch-startTime;
return TaskResult(
cost:cost,
data:result,
taskName: "calculate"
);
}
int loopAdd(int count) {
int sum = 0;
for (int i = 0; i <= count; i++) {
sum+=i;
}
return sum;
}
這里 TaskResult 類用于記錄任務完成的信息:
class TaskResult {
final int cost;
final String taskName;
final dynamic data;
TaskResult({
required this.cost,
required this.data,
required this.taskName,
});
Map<String,dynamic> toJson()=>{
"taskName":taskName,
"cost":cost,
"data": data
};
}
2.Dart 編程中的異步任務
如下在計算之后,還有兩個任務:saveToFile 任務,將運算結(jié)果保存到文件中;以及 render 任務將運算結(jié)果渲染到界面上。
void main() {
TaskResult result = cacaulate();
saveToFile(result);
render(result);
}
這里 render 任務暫時通過在控制臺打印顯示作為渲染,邏輯如下:
void render(TaskResult result) {
print("結(jié)果渲染: ${result.toJson()}");
}
下面是將結(jié)果寫入文件的任務實現(xiàn)邏輯。其中 File 對象的 writeAsString 是一個異步方法,可以將內(nèi)容寫入到文件中。通過 then 方法設置回調(diào),監(jiān)聽任務完成的時機。

void saveToFile(TaskResult result) {
String filePath = path.join(Directory.current.path, "out.json");
File file = File(filePath);
String content = json.encode(result);
file.writeAsString(content).then((File value){
print("寫入文件成功:!${value.path}");
});
}
3.當前任務分析
如下是這三個任務的執(zhí)行示意,在 saveToFile 中使用 writeAsString 方法將異步處理寫入邏輯。

這樣就像在燒水任務分發(fā)后,可以執(zhí)行曬被一樣。saveToFile 任務分發(fā)之后,不需要等待文件寫入完成,可以繼續(xù)執(zhí)行 render 方法。日志輸出如下:渲染任務的執(zhí)行并不會因?qū)懭胛募蝿斩枞?,這就是異步處理的價值。

四、異步模型的延伸
1. 單線程異步模型的局限性
本文主要介紹 異步模型 的概念,認識異步的作用,以及 Dart 編程語言中異步方法的基本使用。至于代碼中更具體的異步使用方式,將在后期文章中結(jié)合詳細介紹。另外,一般情況下,Dart 是以 單線程 運行的,所以本文中強調(diào)的是 單線程 下的異步模型。
仔細思考一下,可以看出,單線程中實現(xiàn)異步是有局限性的。比如說需要解析一個很大的 json ,或者進行復雜的邏輯運算等 耗時任務,這種必須由 本機體 處理的邏輯,而不是 等待結(jié)果 的場景,是無法在單線程中異步處理的。
就像是 掃地 和 曬被 任務,對于單一機體來說,不可能同時參與到兩個任務之中。在實際開發(fā)中這兩個任務可類比為 解析超大 json 和 顯示解析中界面 兩個任務。如果前者耗時三秒,由于單線程 中同步方法的阻塞,界面就會卡住三秒,這就是單線程異步模型的 局限性。
2. 多線程與異步的關(guān)系
上面問題的本質(zhì)矛盾是:一個機體無法 同時 參與到兩件任務 具體執(zhí)行過程中。解決方案也非常簡單,一個人搞不定,就搖人唄。多個機體參與任務分配的場景,就是 多線程 。 很多人都會討論 異步 和 多線程 的關(guān)系,其實很簡單:兩個機體,一個 掃地,一個 曬被,同一時刻,存在兩個及以上的任務在生命期中,一定是異步的。毫無疑問,多線程 是 異步模型 的一種實現(xiàn)方式。

3. Dart 中如何解決單線程異步模型的局限性
像 C++ 、Java 這些語言有 多線程 的支持,通過 “搖人” 可以充分調(diào)度 CPU 核心,來處理一些計算密集型的任務,實現(xiàn)任務在時間上的最合理分配。
絕大多數(shù)人可能覺得 Dart 是一個單線程的編程語言,其實不然??赡苁呛芏嗳瞬]有在 Flutter 端做過計算密集型的任務,沒有對多線程迫切的需要。畢竟 移動/桌面客戶端 大多是網(wǎng)絡、數(shù)據(jù)庫訪問等 io 密集型 的任務,人手一個終端,沒有什么高并發(fā)的場景。不像后端那樣需要保證一個終端被百萬人同時訪問。
或者計算密集型的任務都有由平臺機體進行處理,將結(jié)果通知給 Flutter 端。這導致 Dart 看起來更像是一個 任務分發(fā)者,發(fā)號施令的人,絕大多數(shù)時候并不需要親自參與任務的執(zhí)行過程中。而這正是單線程下的異步模型所擅長的:借他人之力,監(jiān)聽回調(diào)信息。
其實我們在日常開發(fā)中,使用的平臺相關(guān)的插件,其中的方法基本上都是異步的,本質(zhì)上就是這個原因。平臺 是個燒水壺,燒水任務只需要分發(fā) 和 監(jiān)聽回調(diào)。至于水怎么燒開,是 平臺 需要關(guān)心的,這和 網(wǎng)絡 io 、磁盤 io 是很類似的,都是 請求 與 響應 的模式。這種任務,由單線程的異步模型進行處理,是最有效的,畢竟 “搖人” 還是要管飯的。
那如果非要在 Dart 中處理計算密集型的任務,該如何是好呢?不用擔心,Dart 的 isolate 機制可以完成這項需求。關(guān)于這點,在后面會進行詳述。認識 異步 是什么,是本文的核心,那本文就到這里,謝謝觀看 ~
更多關(guān)于Flutter 單線程異步模型的資料請關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
Android程序開發(fā)之WebView使用總結(jié)
這篇文章主要介紹了Android程序開發(fā)之WebView使用總結(jié)的相關(guān)資料,非常不錯,具有參考借鑒價值,需要的朋友可以參考下2016-07-07
Android TextView實現(xiàn)圖文混合編排的方法
這篇文章主要為大家詳細介紹了Android TextView實現(xiàn)圖文混合編排的方法,具有一定的參考價值,感興趣的小伙伴們可以參考一下2017-07-07
Arduino 數(shù)據(jù)類型轉(zhuǎn)換(單機片)詳細介紹
這篇文章主要介紹了Arduino 數(shù)據(jù)類型轉(zhuǎn)換(單機片)詳細介紹的相關(guān)資料,需要的朋友可以參考下2016-11-11
在Android中創(chuàng)建菜單項Menu以及獲取手機分辨率的解決方法
本篇文章小編為大家介紹,在Android中創(chuàng)建菜單項Menu以及獲取手機分辨率的解決方法。需要的朋友參考下2013-04-04
Android?APP瘦身shrinkResources使用問題詳解
這篇文章主要為大家介紹了Android?APP瘦身shrinkResources使用問題詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進步,早日升職加薪2022-11-11

