枯燥的Kotlin協程三部曲(上)——概念篇

0x0、引言


Kotlin 1.3 版本開始引入協程 Coroutine,簡練的官方文檔 和 網上一堆淺嘗輒止的文章讓我內心沒底,不想止步於僅僅知道:css

① Android中,Kotlin協程用於解決:處理耗時任務保證主線程安全
② 利用Kotlin協程,能夠用看起來:同步 的方式編寫 異步 代碼;
③ 基礎的API調用;html

我還想了解更多,如協程的概念,Kotlin協程在實際開發中的使用,背後的原理等,遂有此文。
Kotlin協程的源碼還沒啃完,此係列目前只能算是筆記,邊看邊學,部份內容摘取自參考文獻,只是讓潛意識裏有這些概念,後續看完源碼,縷清思路再來整理一波,有紕漏之處,歡迎評論區友善指出,一塊兒討論學習,謝謝~
本文主要闡述一些概念相關的東西,一些前置知識,有所瞭解得可直接跳過。前端


0x一、追根溯源


一、同步 & 異步


先撇開編程相關的東西不說,經過 坐公交 的形象化例子幫助理解 同步與異步:java

乘客排隊等公交,車來了,前門掃碼上車,一個掃完到下一個掃,一種 串行化 的關係,這是 同步
前門乘客上車,後門乘客下車,互不影響,同時進行,一種 並行化 的關係,這是 異步android

咱們把乘客上車和下車,看作是兩個 任務,司機開車也是一個任務,跟這兩個任務是異步關係。異步說明二者能夠同時進行,乘客還沒上完車,司機直接把車開走,也是能夠的:git

不過這顯然不合常理,正常來講:司機應該等乘客上下車完畢才發車,那司機怎麼知道:github

常規操做有兩種:web

輪詢(主動):每隔一段時間查看下先後門監控,看下還有沒有乘客;
回調(被動):早期的公交車上都會配有一個乘車員,沒乘客上下車了,她就會喊司機開車;編程


二、堵塞 & 非堵塞


同步和異步的關注點是 是否同時進行,而堵塞和非堵塞關注的是 可否繼續進行,仍是坐公交的例子:安全

有乘客上下車,司機發車就須要等待,此時司機發車的任務處於 堵塞 狀態;
乘客都上下車完畢,司機又能夠發車了,此時司機發車的任務處於 非堵塞 狀態;

堵塞的真正含義:關心的事物因爲某些緣由,沒法繼續進行,所以讓你等待。
等待:只是堵塞的一個反作用,代表隨時間流逝,沒有任何有意義的事物發生或進行。

堵塞時,不必乾等着,能夠作點其餘無關的事物,由於這不影響你對相關事情的等待;
好比司機等發車時,能夠喝喝茶、看看手機等,但不能離開。

計算機沒人那麼靈活,堵塞時乾等最容易實現,只需掛起線程,讓出CPU便可,等條件知足時,在從新調度此線程。


三、程序


回到編程相關,任務 對應計算機中的 程序,定義以下:

爲了完成特定任務,用某種編程語言編寫的一組指令集合(一組 靜態代碼)

CPU處理器逐條執行指令,哪怕出現外部中斷,也只是從當前程序切到另外一段程序,繼續逐條執行。

和預期一致,代碼 逐條執行,但有些業務場景 順序結構 就無能爲力了,好比:

女友:你下班後去超市買10個雞蛋回來,看到有賣西瓜的就買1個

此時,須要用到四種 基礎控制流 中的另一種 → 選擇執行

剩下兩種基礎控制流爲 迭代和遞歸,咱們使用 控制流 來完成 邏輯流,程序執行到哪,邏輯就執行到哪,這樣的程序結構清晰,可讀性好,比較符合編程人員的思惟習慣,這也是 同步編程 的方式。


四、進程


同一時刻只有一個程序在內存中被CPU調用運行

假設有A、B兩個程序,A正在運行,此時須要讀取大量輸入數據(IO操做),那麼CPU只能乾等,直到A數據讀取完畢,再繼續往下執行,A執行完,再去執行程序B,白白浪費CPU資源。

看着有點蠢,能不能這樣:

當程序A讀取數據的時,切換 到程序B去執行,當A讀取完數據,讓程序B暫停,切換 回程序A執行?

固然能夠,不過在計算機裏 切換 這個名詞被細分爲兩種狀態:

掛起:保存程序的當前狀態,暫停當前程序;
激活:恢復程序狀態,繼續執行程序;

這種切換,涉及到了 程序狀態的保存和恢復,並且程序A和B所需的系統資源(內存、硬盤等)是不同的,那還須要一個東西來記錄程序A和B各自須要什麼資源,還有系統控制程序A和B切換,要一個標誌來識別等等,因此就有了一個叫 進程的抽象

進程的定義

程序在一個數據集上的一次動態執行過程,通常由下述三個部分組成:

  • 程序:描述進程要完成的功能及如何完成;
  • 數據集:程序在執行過程當中所需的資源;
  • 進程控制塊:記錄進程的外部特徵,描述執行變化過程,系統利用它來控制、管理進程,系統感知進程存在的惟一標誌。

進程是系統進行 資源分配和調度 的一個 獨立單位

進程的出現使得多個程序得以 併發 執行,提升了系統效率及資源利用率,但存在下述問題:

① 單個進程只能幹一件事,進程中的代碼依舊是串行執行。
② 執行過程若是堵塞,整個進程就會掛起,即便進程中某些工做不依賴於正在等待的資源,也不會執行。
③ 多個進程間的內存沒法共享,進程間通信比較麻煩。

因而劃分粒度更小的 線程 出現了。


五、線程

線程的出現是爲了下降上下文切換消耗,提升系統的併發性,並突破一個進程只能幹一件事的缺陷,使得 進程內併發 成爲可能。

線程的定義

輕量級的進程,基本的CPU執行單元,亦是 程序執行過程當中的最小單元,由 線程ID、程序計數器、寄存器組合和堆棧 共同組成。線程的引入減少了程序併發執行時的開銷,提升了操做系統的併發性能。

區分:「進程」是「資源分配」的最小單位,「線程」是 「CPU調度」的最小單位

線程和進程的關係

① 一個程序至少有一個進程,一個進程至少有一個線程,能夠把進程理解作 線程的容器
② 進程在執行過程當中擁有 獨立的內存單元,該進程裏的多個線程 共享內存
③ 進程能夠拓展到 多機,線程最多適合 多核
④ 每一個獨立線程有一個程序運行的入口、順序執行列和程序出口,但不能獨立運行,需依存於應用程序中,由應用程序提供多個線程執行控制;

進程和線程都是一個時間段的描述,是 CPU工做時間段的描述,只是顆粒大小不一樣。


六、併發 & 並行


上面提到一個名詞 併發 (Concurrency),指的是:

同一時刻只有一條指令執行,但多個進程指令被快速地 輪換執行,使得在宏觀上有同時執行的效果,微觀上並非同時執行,只是把CPU時間分紅若干段,使得多個進程快速交替地執行,存在於單核或多核CPU系統中。

而另外一個容易混淆的名詞 並行 (Parallel) 則是:

同一時刻,有多條指令在多個處理器上同時執行,從微觀和宏觀上看,都是一塊兒執行的,存在於多核CPU系統中。


七、協做式 & 搶奪式


單核CPU,同一時刻只有一個進程在執行,這麼多進程,CPU的時間片該如何分配呢?

協做式多任務

早期的操做系統採用的就是協做時多任務,即:

由進程主動讓出執行權,如當前進程需等待IO操做,主動讓出CPU,由系統調度下一個進程。

每一個進程都循規蹈矩,該讓出CPU就讓出CPU,是挺和諧的,但也存在一個隱患:

單個進程能夠徹底霸佔CPU

計算機中的進程參差不齊,先不說那種居心叵測的進程了,若是是健壯性比較差的進程,運行中途發生了死循環、死鎖等,會致使整個系統陷入癱瘓!在這種魚龍混雜的大環境下,把執行權託付給進程自身,確定是不符合基礎國情,由操做系統扛大旗的 搶佔式多任務 橫空出世~

搶佔式多任務

由操做系統決定執行權,操做系統具備從任何一個進程取走控制權和使另外一個進程得到控制權的能力。

系統公平合理地爲每一個進程分配時間片,進程用完就休眠,甚至時間片沒用完,但有更緊急的事件要優先執行,也會強制讓進程休眠。有了進程設計的經驗,線程也作成了搶佔式多任務,但也帶來了新的——線程安全問題


八、線程安全問題


進程在執行過程當中擁有獨立的內存單元,而多個線程共享這個內存,可能存在這樣一種狀況:

假設有一個變量a = 10,它能夠被線程t1和t2共享訪問,兩個線程都會對i值進行寫入,假設在單核CPU上運行此程序,系統須要給兩個線程都分配CPU時間片:

  • 1.t1從內存中讀取了a的值爲10,它把a的值+1,準備把11這個新值寫入內存中,此時時間片耗盡;
  • 2.系統執行了線程調度,t1的執行現場被保存,t2得到執行,它也去讀a的值,此時a的值仍爲10,+1,而後把11寫入內存中;
  • 3.t1再次被調度,此時它也把11寫入內存中。

程序的執行結果和咱們的預期不符,a的值應該爲12而不是11,這就是線程調度不可預測性引發的 線程同步安全問題

解決方法

系列化訪問臨界資源,同一時刻,只能有一個線程訪問臨界資源,也稱 同步互斥訪問,一般的操做就是 加鎖(同步鎖),當線程訪問臨界資源時須要得到這個鎖,其餘線程沒法訪問,只能 等待(堵塞),等這個線程使用完釋放鎖,供其餘線程繼續訪問。

前置概念相關的東西就說這麼多,相信會對你接下來學習Kotlin協程大有裨益。


0x二、單線程的Android GUI系統


是的,Android GUI 被設計成單線程了,你可能會問:爲啥不採用性能更高的多線程?

答:若是設計成多線程,多個線程同時對一個UI控件進行更新,容易發生 線程同步安全問題;最簡單的解決方式:加鎖,但這意味着更多的耗時和UI更新效率的下降,並且還有死鎖等諸多問題要解決;多線程模型帶來的複雜度成本,遠遠超出它能提供的性能優點成本。這也是大部分GUI系統都是單線程模型的緣由。

Android要求:在主線程(UI線程)更新UI,注意是 → 要求建議,不是規定,規定底線是:

只有建立這個view的線程才能操做這個view

因此,你在子線程中更新子線程建立的UI也是能夠的,不過不建議這麼作,建議:

在子線程中完成耗時操做,而後經過Handler發送消息,通知UI線程更新UI。

Tips:關於Handler更多的內容可移步至:《換個姿式,帶着問題看Handler》

接着說下,Android異步更新UI的寫法都有哪些~


一、Handler


主線程中實例化一個Handler對象,在子線程須要更新UI的地方,經過Handler對象的post(runnable)或其餘函數,往主線程的消息隊列發送消息,等待調度器調度,分發給對應的Handler完成UI更新,寫法示例以下:

利用 lambda表達式 + Kotlin語法糖thread { } 可對上述代碼進行簡化:

還有另一種常見的寫法:自定義一個靜態內部類Handler,把UI更新操做統一放到這裏,根據msg.what進行區分。


二、AsyncTask


AsyncTask是Android提供的一個輕量級的用於處理異步任務的類(封裝Handler+Thread),使用代碼示例以下:

相比起手寫Handler簡單了一些,只須要繼承AsyncTask,而後就是填空題(按需重寫函數):

  • onPreExecute():異步操做開始,能夠作一些UI的初始化操做;
  • doInBackground():執行異步操做,可調用publishProgress()觸發onProgressUpdate()進度更新;
  • onProgressUpdate():根據進度更新UI;
  • onPostExecute():異步操做完成,更新UI;

但也存在如下侷限性:

① AsyncTask類需在主線程中加載;
② AsyncTask對象需在主線程中建立;
③ execute()必須在主線程中調用,且一個AsyncTask對象只能調用一次此方法;
④ 須要爲每一種任務類型建立一個特定子類,同時爲了訪問UI方便,常常定義爲Activity的內部類,耦合嚴重。

能夠經過 函數轉換爲回調的方式 來解耦,抽取後的自定義AsyncTask類以下:

調用也很簡單,按需重寫對應函數便可:

解耦後靈活多了,外部調用邏輯內部異步邏輯 的分離開來了,但依舊存在問題,如異常處理、任務取消等。


三、runOnUiThread


能夠說是很無腦了,在子線程中想更新UI,直接寫一個runOnUiThread{}包裹着UI更新相關的代碼便可,示例以下:

點進源碼康康:

噢,還挺簡單:

Activity中定義了此函數,判斷當前線程是否爲主線程,不是 → Handler.post,是 → 直接執行UI更新。

回調確實是個好東西,可是多層次的回調嵌套,可能會造成 Callback Hell(回調地獄),好比如今有這樣的邏輯:

訪問百度 → 展現內容(UI) → 下載圖標 → 顯示圖標(UI) → 生成縮略圖 → 顯示縮略圖(UI) → 上傳縮略圖 → 界面更新(UI)

按照這樣的邏輯,用runOnUiThread一把梭,僞代碼以下:

老千層餅了,一層套一層,看到這種代碼,不知道你是否是和我同樣 氣抖冷 (據說前端寫js回調的更可怕,23333)

常見的規避方法:把嵌套的層次移到外層空間,不使用匿名的回調函數,爲每一個回調函數命名。


四、RxJava

RxJava在 鏈式調用 的設計基礎上,經過設置 不一樣的調度器,能夠靈活地在 不一樣線程間切換 並執行對應的Task。
RxJava很強大,但由於較高的學習門檻,大多Android開發仔的認知還停留在:線程切換工具+操做符好用 的階段。
巧了,筆者也是:

有興趣深刻學習的RxJava的能夠康康《RxJava 沉思錄(一):你認爲 RxJava 真的好用嗎?》
這裏只是展現效果,用RxJava寫代碼的效果,等我變強了,再回來完善這一塊:

輸出結果以下:

爲所欲爲,控制線程切換~


五、LiveData


LiveData 是Jetpack提供的一種響應式編程組件,能夠包含任何類型的數據,並在數據發生變化時通知給觀察者;因爲它能夠感知並遵循Activity、Fragment或Service等組件的生命週期,所以能夠作到僅在組件處於聲明週期的激活狀態時才更新UI。通常是搭配 ViewModel 組件一塊兒使用的。

MutableLiveData是一種可變的LiveData,提供了兩種讀數據的方法:
主線程中調用的setValue()在非主線程中調用的postValue()

使用時需導入依賴

implementation "androidx.lifecycle:lifecycle-runtime:2.2.0"
複製代碼

使用代碼示例以下


六、Kotlin協程


使用Kotlin協程須要先添加 協程核心庫和平臺庫 依賴(build.gradle中引入):

implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.3.7'
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.7'
複製代碼

使用 withContext 函數能夠切換到指定的線程,並在閉包內的邏輯執行結束後,自動把線程切換回上下文繼續執行。把RxJava部分的示例改爲Kotlin協程的形式,代碼示例以下:

使用Kotlin協程後,代碼量並無減小,可是異步代碼的編寫卻輕鬆多了,開始有一種「同步方式寫異步代碼」的味道了~
再簡化下,把withContext 做爲函數的返回值。


0x三、Kotlin中的協程究竟是什麼


協程

一種 非搶佔式(協做式) 的 任務調度模式,程序能夠 主動掛起或者恢復執行

與線程的關係

協程基於線程,但相對於線程輕量不少,可理解爲在用戶層模擬線程操做;每建立一個協程,都有一個內核態進程動態綁定,用戶態下實現調度、切換,真正執行任務的仍是內核線程。線程的上下文切換都須要內核參與,而協程的上下文切換,徹底由用戶去控制,避免了大量的中斷參與,減小了線程上下文切換與調度消耗的資源。

根據 是否開闢相應的函數調用棧 又分紅兩類:

  • 有棧協程:有本身的調用棧,可在任意函數調用層級掛起,並轉移調度權;
  • 無棧協程:沒有本身的調用棧,掛起點的狀態經過狀態機或閉包等語法來實現;

Kotlin中的協程

"假"協程,Kotlin在語言級別並無實現一種同步機制(鎖),仍是依靠Kotlin-JVM的提供的Java關鍵字(如synchronized),即鎖的實現仍是交給線程處理,於是Kotlin協程本質上只是一套基於原生Java Thread API 的封裝。

只是這套API 隱藏了異步實現細節,讓咱們能夠用如同 同步的寫法來寫異步操做 罷了。


參考文獻

相關文章
相關標籤/搜索