前言
協程系列文章:
相信稍微接觸過Kotlin的同學都知道Kotlin Coroutine(協程)的大名,甚至有些同學認為重要到"無協程,不Kotlin"的地步,嚇得我趕緊去翻閱了協程源碼,同時也學習了不少博客,博客里比較典型的幾個說法:
- 協程是輕量級線程、比線程耗費資源少
- 協程是線程框架
- 協程效率高于線程
- ...
一堆術語聽起來是不是很高端的樣子?這些表述正確嗎?妥當嗎?你說我學了大半天,雖然我也會用,但還是沒弄懂啥是協程...
為了徹底弄懂啥是協程,需要將進程、線程拉進來一起pk。
通過本篇文章,你將了解到:
1、程序、進程、CPU、內存關系
2、進程與線程的故事
3、線程與Kotlin協程的故事
4、Kotlin 協程的使命
1、程序、進程、CPU、內存關系
如上圖,平時我們打包好一個應用,放在磁盤上,此時我們稱之為程序或者應用,是靜態的。也就是咱們平常說的:我下載個程序,你傳給apk給我,它們都是程序(應用)。
當我們執行程序(比如點擊某個App),OS 會將它加載進內存,CPU 從內存某個起始地址開始讀取指令并執行程序。
程序從磁盤上加載到內存并被CPU運行期間,稱之為進程。因此我們通常說某個應用是否還在存活,實際上說的是進程是否還在內存里;也會說某某程序CPU占用率太高,實際上說的是進程的CPU占用率。
而操作系統負責管理磁盤、內存、CPU等交互,可以說是大管家。
2、進程與線程的故事
接下來我們以一個故事說起。
上古時代的合作
在上古時候,一個設備里只有一個CPU,能力比較弱,單位時間內能夠處理的任務量有限,內存比較小,能加載的應用不多,相應的那會兒編寫的程序功能單一,結構簡單。
OS 說:"大家都知道,我們的情況比較具體,只有一個CPU,內存也很小,而現在有不少應用想要占用CPU和內存,無規矩不成方圓,我現在定下個規矩:"
每個應用加載到內存后,我將給他安排內存里的一塊獨立的空間,并記錄它的一些必要信息,最后規整為一個叫進程的東西,就是代表你這個應用的所有信息,以后我就只管調度進程即可。
并且進程之間的內存空間是隔離的,無法輕易訪問,特殊情況需要經過我的允許。
應用(程序)說:"哦,我知道了,意思就是:進程是資源分派的基本單位嘛"
OS 說:"對的,悟性真好,小伙子。"
規矩定下了,大家就開始干活了:
1、應用被加載到內存后,OS分派了一些資源給它。
2、CPU 從內存里逐一取出并執行進程。
3、其它沒有得到CPU青睞的進程則靜候等待,等待被翻牌。
中古時代的合作
一切都有條不紊的進行著,大家配合默契,其樂融融,直到有一天,OS 發現了一些端倪。
他發現CPU 在偷懶...找到CPU,壓抑心中的憤怒說到:
"我發現你最近不是很忙哎,是不是工作量不飽和?"
CPU 忙不迭說到:"冤枉啊,我確實不是很忙,但這不怪我啊。你也知道我最近升級了頻率,處理速度快了很多,進程每次給我的任務我都快速執行完了,不過它們卻一直占用我,不讓我處理其它進程的,我也是沒辦法啊。"
OS 大吃一驚到:"大膽進程,居然占著茅坑不拉屎!"
CPU 小聲到:"我又不是茅坑..."
OS 找來進程劈頭蓋臉訓斥一道:"進程你好大的膽,我之前不是給你說請CPU 做事情要講究一個原則:按需占有,用完就退。你把我話當耳邊風了?"
進程直呼:"此事與我無關啊,你知道的我最講原則了,你之前說過對CPU 的使用:應占盡占。我現在不僅要處理本地邏輯,還要從磁盤讀取文件,這個時候我雖然不占用CPU,但是我后面文件讀結束還是需要他。"
OS 眉頭緊皺,略微思索了一下對進程和CPU道:"此事前因后果均已知悉,容我斟酌幾日。"
幾天后,OS 過來對他倆說:"我現在重新擬定一個規則:進程不能一直占用CPU到任務結束為止,需要規定占用的時間片,在規定的時間片內進程能完成多少是多少,時間一到立即退出CPU換另一個進程上,沒能完成任務的進程等下個輪到自己的時間片再上"
進程和CPU 對視一眼,立即附和:"謹遵鈞令,使命必達!"
近現代的合作
自從實行新規定以來,進程們都有機會搶占CPU了,算是雨露均沾,很少出現某進程長期霸占CPU的現象了,OS 對此很是滿意。
一則來自進程的舉報打破這黎明前的寧靜。
OS 收到一則舉報:"我進程實名舉報CPU 偷懶。"
OS 心里咯噔一跳,尋思著咋又是CPU,于是叫來CPU 對簿公堂。
CPU 聽到OS 召喚,暗叫不妙,心里立馬準備了一套說辭。
OS 對著CPU 和 進程說:"進程說你偷懶,你在服務進程的時間片內無所事事,我希望你能給我一個滿意的答復。"
CPU 一聽這話,心里一陣鄙視,果不出我所料,就知道你問這事。雖然心里誹腹不已,臉上卻是鄭重其事道:"這事是因為進程交給我的任務很快完成了,它去忙別的事了,讓我等等他。"
OS 詫異道:"你這么快就將進程的任務處理完成了?"
CPU 面露得以之色道:"你知道的我一直追求進步,這不前陣子又升級了一下嘛,處理能力又提升了。如果說優秀是一種原罪的話,那這個罪名由我承擔吧,再如果..."
OS 看了進程一眼,對CPU 說:"行行行,打住,此事確實與你無關。進程雖然你誤會了CPU,但是你提出的問題確實是一個好的思考點,這個下來我想個方案,回頭咱們評審一下。"
一個月后,OS 將進程和CPU召集起來,并拿出方案說:"我們這次將進行一次大的調整,鑒于CPU 處理能力提升,他想要承擔更多的工作,而目前以進程為單位提交任務顆粒度太大了,需要再細化。我建議將進程劃分為若干線程,這些線程共享進程的資源池,進程想要執行某任務直接交給線程即可,而CPU每次以線程為單位執行。接下來,你們說說各自的意見吧。"
進程說到:"這個方案很優秀,相當于我可以弄出分身,讓各個分身干各項任務,處理UI一個線程,處理I/O是另一個線程,處理其它任務是其它線程,我只需要分派各個任務給線程,剩下的無需我操心了。CPU 你覺得呢?"
CPU 心底暗道:"你自己倒是簡單,只管造分身,臟活累活都是我干..."
表面故作沉重說到:"這個改動有點大,我現在需要直接對接線程,這塊需要下來好好研究一下,不過問題不大。"
進程補充道:"CPU 你可以要記清楚了,以后線程是CPU 調度的基本單位了。"
CPU 應道:"好的,好的,了解了(還用你復述OS 的話嘛...)。"
規矩定下了,大家熱火朝天地干活。
進程至少有一個線程在運行,其余按需制造線程,多個線程共用進程資源,每個線程都被CPU 執行。
新時代的合作
OS 照例視察各個模塊的合作,這天進程又向它抱怨了:"我最近各個線程的數據總是對不上,是不是內存出現了差錯?"
OS 愣了一下,說到:"這問題我知道了,還沒來得及和你說呢。最近咱們多放了幾個CPU 模塊提升設備的整體性能,你的線程可能在不同的CPU上運行,因此拿到的數據有點問題。"
進程若有所思道:"以前只有一個CPU,各個進程看似同時運行,實則分享CPU時間片,是并發行為。現在CPU 多了,不同的線程有機會同時運行,這就是真并行了吧。"
OS 道:"舉一反三能力不錯哦,不管并行還是并發,多個線程共享的數據有可能不一致,尤其加入了多CPU后,現象比較明顯,這就是多線程數據安全問題。底層已經提供了一些基本的機制,比如CPU的MESI,但還是無法完全解決這問題,剩下的交給上層吧。"
進程道:"了解了,那我告訴各個線程,如果他們有共享數據的需求,自己協商解決一下。"
進程告知線程自己處理線程安全問題,線程答到:"我只是個工具人,誰用誰負責處理就好。"
一眾編程語言答到:"我自己來處理吧。"
多CPU 如下,每個線程都有可能被其它CPU運行。
3、線程與Kotlin協程的故事
Java 線程調用
底層一眾大佬已經將坑踩得差不多了,這時候得各個編程語言出場了。
C 語言作為骨灰級人物遠近聞名,OS、驅動等都是由他編寫,這無需介紹了。
之后如雨后春筍般又冒出了許多優秀的語言,如C++、Java、C#、Qt 等,本小結的主人公:Java。
Java 從小目標遠大,想要跨平臺運行,借助于JVM他可以實現這個夢想,每個JVM 實例對應一個進程,并且OS 還給了他操作線程的權限。
Java 想既然大佬這么支持,那我要擼起袖子加油干了,剛好在Android 上接到一個需求:
通過學生的id,向后臺(聯網)查詢學生的基本信息,如姓名、年齡等。
Java 心想:"這還不簡單,且看我猛如虎的操作。"
先定義學生Bean類型:
public class StudentInfo {
//學生id
private long stuId = 999;
private String name = "fish";
private int age = 18;
}
再定義一個獲取的動作:
//從后臺獲取信息
public StudentInfo getWithoutThread(long stuId) {
try {
//模擬耗時操作
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
return new StudentInfo();
}
信心滿滿地運行,卻被現實無情打臉,只見控制臺顯目的紅色:
不能在主線程進行網絡請求。
同步調用
Java 并不氣餒,這問題簡單,我開個線程取獲取不就得了?
Callable<StudentInfo> callable = new Callable<StudentInfo>() {
@Override
public StudentInfo call() throws Exception {
try {
//模擬耗時操作
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
return new StudentInfo();
}
};
public StudentInfo getStuInfo(long stuId) {
//定義任務
FutureTask<StudentInfo> futureTask = new FutureTask<>(callable);
//開啟線程,執行任務
new Thread(futureTask).start();
try {
//阻塞獲取結果
StudentInfo studentInfo = futureTask.get();
return studentInfo;
} catch (ExecutionException e) {
e.printStackTrace();
} catch (InterruptedException e) {
e.printStackTrace();
}
return null;
}
而后,再在界面上彈出學生姓名:
JavaStudent javaStudent = new JavaStudent();
StudentInfo studentInfo = javaStudent.getWithoutThread(999);
Toast.makeText(this, "學生姓名:" + studentInfo.getName(), Toast.LENGTH_LONG).show();
剛開始能彈出Toast,然而后面動不動UI就卡頓,甚至出現ANR 彈窗。
Java 百思不得其解,后得到Android 本尊指點:
Android 主線程不能進行耗時操作。
Java 說到:"我就簡單獲取個信息,咋這么多限制..."
Android 答到:"Android 通常需要在主線程更新UI,主線程不能做過多耗時操作,否則影響UI 渲染流暢度。不僅是Android,你Java 本身的主線程(main線程)通常也不會做耗時啊,都是通過開啟各個線程去完成任務,要不然每一步都要主線程等待,那主線程的其它關鍵任務就沒法開啟了。"
Java 沉思道:"有道理,容我三思。"
異步調用與回調
Java 果不愧是編程語言界的老手,閉關幾天就想出了方案,直接show us code:
//回調接口
public interface Callback {
void onCallback(StudentInfo studentInfo);
}
//異步調用
public void getStuInfoAsync(long stuId, Callback callback) {
new Thread(() -> {
try {
//模擬耗時操作
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
StudentInfo studentInfo = new StudentInfo();
if (callback != null) {
//回調給調用者
callback.onCallback(studentInfo);
}
}).start();
}
在調用耗時方法時,只需要將自己的憑證(回調對象)傳給方法即可,調用者不管方法里具體是咋實現的,才不管你開幾個線程呢,反正你有結果通過回調給我。
調用者只需要在需要的地方實現回調接收即可:
JavaStudent javaStudent = new JavaStudent();
javaStudent.getStuInfoAsync(999, new JavaStudent.Callback() {
@Override
public void onCallback(StudentInfo studentInfo) {
//異步調用,回調從子線程返回,需要切換到主線程更新UI
runOnUiThread(() -> {
Toast.makeText(TestJavaActivity.this, "學生姓名:" + studentInfo.getName(), Toast.LENGTH_LONG).show();
});
}
});
異步調用的好處顯而易見:
1、不用阻塞調用者,調用者可繼續做其它事情。
2、線程沒有被阻塞,相比同步調用效率更高。
缺點也是比較明顯:
1、沒有同步調用直觀。
2、容易陷入多層回調,不利于閱讀與調試。
3、從內到外的異常處理缺失傳遞性。
Kotlin 協程毛遂自薦
Java 靠著左手同步調用、右手異步調用的左右互搏技能,成功實現了很多項目,雖然異步調用有著一些缺點,但瑕不掩瑜。
這天,Java 又收到需求變更了:
通過學生id,獲取學生信息,通過學生信息,獲取他的語文老師id,通過語文老師id,獲取老師姓名,最后更新UI。
Java 不假思索到:"簡單,我再嵌套一層回調即可。"
//回調接口
public interface Callback {
void onCallback(StudentInfo studentInfo);
//新增老師回調接口
default void onCallback(TeacherInfo teacherInfo){}
}
//異步調用
public void getTeachInfoAsync(long stuId, Callback callback) {
//先獲取學生信息
getStuInfoAsync(stuId, new Callback() {
@Override
public void onCallback(StudentInfo studentInfo) {
//獲取學生信息后,取出關聯的語文老師id,獲取老師信息
new Thread(() -> {
try {
//模擬耗時操作
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
TeacherInfo teacherInfo = new TeacherInfo();
if (callback != null) {
//老師信息獲取成功
callback.onCallback(teacherInfo);
}
}).start();
}
});
}
眼看Java 一下子實現了功能,Android再提需求:
通過老師id,獲取他所在的教研組信息,再通過教研組id獲取教研組排名...
Java 抗議道:"哪有這么奇葩的需求,那我不是要無限回調嗎,我可以實現,但不好維護,過幾天我自己看都看不懂了。"
Android:"不就是幾個回調的問題嘛,虧你還是老員工,實在不行,我找其他人。"
Java:"...我再想想。"
正當Java 一籌莫展之際,吃飯時剛好碰到了Kotlin,Java 難得有時間和這位新入職的小伙伴聊聊天,發發牢騷。
Kotlin 聽了Java 的遭遇,表達了同情并勸說Java 趕緊離職,Android 這塊不適合他。
Kotlin 隨后找到Android,略微緊張地說:"吾有一計,可安天下。"
Android 對于毛遂自薦的人才是非常歡迎的,問曰:"計將安出"
Kotlin 隨后激動到:協程。
Android 詫異道:"協程,旅游?"
Kotlin 趕緊道:"非也,此協程非彼攜程...而是它"
Android 說:"看這肌肉挺大的,想必比較強,請開始你的表演吧。"
Koltin 立馬展示自己。
class StudentCoroutine {
private val FIXED_TEACHER_ID = 888
fun getTeachInfo(act: Activity, stuId: Long) {
GlobalScope.launch(Dispatchers.Main) {
var studentInfo: StudentInfo
var teacherInfo: TeacherInfo? = null
//先獲取學生信息
withContext(Dispatchers.IO) {
//模擬網絡獲取
Thread.sleep(2000)
studentInfo = StudentInfo()
}
//再獲取教師信息
withContext(Dispatchers.IO) {
if (studentInfo.lanTechId.toInt() === FIXED_TEACHER_ID) {
//模擬網絡獲取
Thread.sleep(2000)
teacherInfo = TeacherInfo()
}
}
//更新UI
Toast.makeText(act, "teacher name:${teacherInfo?.name}", Toast.LENGTH_LONG).show()
}
Toast.makeText(act, "主線程還在跑...", Toast.LENGTH_LONG).show()
}
}
外部調用:
var student = StudentCoroutine()
student.getTeachInfo(this@MainActivity, 999)
Android 一看,大吃一驚:"想不到,語言界竟然有如此厚顏無恥之...不對,如此簡潔的寫法。"
Kotlin 道:"協程這概念早就有了,其它兄弟語言Python、Go等也實現了,我也是站在巨人的肩膀上,秉著解決用戶痛點的思路來設計的。"
Android 隨即大手一揮道:"就沖著你這簡潔的語法,今后Android 業務你來搞吧,希望你能夠擔起重擔。"
Kotlin 立馬道:"沒問題,我本身也是跨平臺的,只是Java 那邊...。"
Android:"這個你無需顧慮,Java 的工作我來做,成年人應該知道這世界是殘酷的。"
Java 聽到Kotlin 逐漸蠶食了自己在Android上的業務,略微生氣,于是看了Kotlin 的寫法,最后長舒一口氣:"確實比較簡潔,看起來功能阻塞了主線程,實際并沒有。其實就是 用同步的寫法,表達異步的調用。"
Koltin :"知我者,老大哥Java 也。"
4、Kotlin 協程的使命
通過與Java 的比對,大家也知道了協程最大的特色:
將異步編程同步化。
當然還有一些特點,如異常處理、協程取消等。
再回過頭來看看上面的疑問。
1、協程是輕量級線程、比線程耗費資源少
這話雖然是官方說的,但我覺得有點誤導的作用,協程是語言層面的東西,線程是系統層面的東西,兩者沒有可比性。
協程就是一段代碼塊,既然是代碼那就離不開CPU的執行,而CPU調度的基本單位是線程。
2、協程是線程框架
協程解決了移步編程時過多回調的問題,既然是異步編程,那勢必涉及到不同的線程。Kotlin 協程內部自己維護了線程池,與Java 線程池相比有些優化的地方。在使用協程過程中,無需關注線程的切換細節,只需指定想要執行的線程即可,從對線程的封裝這方面來說這說話也沒問題。
3、協程效率高于線程
與第一點類似,協程在運行方面的高效率其實換成回調方式也是能夠達成同樣的效果,實際上協程內部也是通過回調實現的,只是在編譯階段封裝了回調的細節而已。因此,協程與線程沒有可比性。
閱讀完上述內容,想必大家都知道進程、線程、協程的關系了,也許大家還很好奇協程是怎么做到不阻塞調用者線程的?它又是怎么在獲取結果后回到原來的位置繼續執行呢?線程之間如何做到絲滑般切換的?
不要著急,這些點我們一點點探秘,下篇文章開始徒手開啟一個協程,并分析其原理。
Kotlin 源碼閱讀需要一定的Kotlin 基礎,尤其是高階函數,若是這方面還不太懂的同學可以查閱之前的文章:Kotlin 高階函數從未如此清晰 系列
本文基于Kotlin 1.5.3,文中完整Demo請點擊
您若喜歡,請點贊、關注,您的鼓勵是我前進的動力
持續更新中,和我一起步步為營系統、深入學習Android/Kotlin
1、Android各種Context的前世今生
2、Android DecorView 必知必會
3、Window/WindowManager 不可不知之事
4、View Measure/Layout/Draw 真明白了
5、Android事件分發全套服務
6、Android invalidate/postInvalidate/requestLayout 徹底厘清
7、Android Window 如何確定大小/onMeasure()多次執行原因
8、Android事件驅動Handler-Message-Looper解析
9、Android 鍵盤一招搞定
10、Android 各種坐標徹底明了
11、Android Activity/Window/View 的background
12、Android Activity創建到View的顯示過
13、Android IPC 系列
14、Android 存儲系列
15、Java 并發系列不再疑惑
16、Java 線程池系列
17、Android Jetpack 前置基礎系列
18、Android Jetpack 易懂易學系列
19、Kotlin 輕松入門系列