線程與協(xié)程關(guān)系:
協(xié)程雖然不能脫離線程而運(yùn)行,但可以在不同的線程之間切換。
我為什么要用上協(xié)程呢?
Kotlin 協(xié)程的核心競爭力在于:它能簡化異步并發(fā)任務(wù)。
比如需要做如下的一個(gè)功能:
查詢用戶信息 --> 查找該用戶的好友列表 -->拿到好友列表后,查找該好友的動(dòng)態(tài)
用地獄回調(diào)寫法如下:
getUserInfo(new CallBack() {
@Override
public void onSuccess(String user) {
if (user != null) {
System.out.println(user);
getFriendList(user, new CallBack() {
@Override
public void onSuccess(String friendList) {
if (friendList != null) {
System.out.println(friendList);
getFeedList(friendList, new CallBack() {
@Override
public void onSuccess(String feed) {
if (feed != null) {
System.out.println(feed);
}
}
});
}
}
});
}
}
});
而使用協(xié)程的寫法代碼就是如此清晰:
launch {
val user = getUserInfo()
val friendList = getFriendList(user)
val feedList = getFeedList(friendList)
}
把異步的方法,寫成掛起函數(shù),用寫同步的代碼完成各個(gè)異步的回調(diào)。
suspend fun getUserInfo(): String {
withContext(Dispatchers.IO) {
delay(1000L)
}
return "BoyCoder"
}
suspend fun getFriendList(user: String): String {
withContext(Dispatchers.IO) {
delay(1000L)
}
return "Tom, Jack"
}
suspend fun getFeedList(list: String): String {
withContext(Dispatchers.IO) {
delay(1000L)
}
return "{FeedList..}"
}
協(xié)程掛起實(shí)現(xiàn)原理
定義掛起函數(shù)
suspend fun requestLoadUser(id: String)
掛起函數(shù)spspend的作用:
聲明suspend函數(shù),它是提醒開發(fā)者,這個(gè)方法里面是個(gè)耗時(shí)函數(shù),如果你在主線程要調(diào)用,你得在協(xié)程中調(diào)用,并且切換到其它線程去調(diào)用,否則會(huì)卡頓主線程。
原本需要異步執(zhí)行并使用回調(diào)的寫法,用同步的寫法,用掛起和恢復(fù)進(jìn)行實(shí)現(xiàn)。
協(xié)程的掛起(suspend)和恢復(fù)(resume):
Kotlin 的編譯器檢測到 suspend 關(guān)鍵字修飾的函數(shù)以后,會(huì)自動(dòng)將掛起函數(shù)轉(zhuǎn)換成帶有回調(diào)的函數(shù)。
反編譯之后:
// Continuation 等價(jià)于 CallBack
// ↓
public static final Object getUserInfo(Continuation $completion) {
...
return "BoyCoder";
可以看到,多了個(gè)Continuation(繼續(xù))參數(shù),這是個(gè)接口,是在本次函數(shù)執(zhí)行完畢后執(zhí)行的回調(diào)。
Continuation的代碼:
public interface Continuation<in T> {
/**
* 保存上下文(比如變量狀態(tài))
*/
public val context: CoroutineContext
/**
* 方法執(zhí)行結(jié)束的回調(diào),參數(shù)是個(gè)泛型,用來傳遞方法執(zhí)行的結(jié)果
*/
public fun resumeWith(result: Result<T>)
}
suspend代碼示例:
suspend fun getToken(id: String): String = "token"
suspend fun getInfo(token: String): String = "info"
// 添加了局部變量a,看下suspend怎么保存a這個(gè)變量
suspend fun test() {
val token = getToken("123") // 掛起點(diǎn)1,這里是異步線程
var a = 10 // 這里是10 //主線程
val info = getInfo(token) // 掛起點(diǎn)2,需要將前面的數(shù)據(jù)保存(比如a),在掛起點(diǎn)之后恢復(fù) //異步線程
println(info) //主線程
println(a
}
反編譯之后:
public final Object getToken(String id, Continuation completion) {
return "token";
}
public final Object getInfo(String token, Continuation completion) {
return "info";
}
// 重點(diǎn)函數(shù)(偽代碼)
public final Object test(Continuation<String>: continuation) {
Continuation cont = new ContinuationImpl(continuation) {
int label; // 保存狀態(tài)
Object result; // 保存中間結(jié)果,還記得那個(gè)Result<T>嗎,是個(gè)泛型,因?yàn)榉盒筒脸詾镺bject,用到就強(qiáng)轉(zhuǎn)
int tempA; // 保存上下文a的值,這個(gè)是根據(jù)具體代碼產(chǎn)生的
};
switch(cont.label) {
case 0 : {
cont.label = 1; //更新label
getToken("123",cont) // 執(zhí)行對應(yīng)的操作,注意cont,就是傳入的回調(diào)
break;
}
case 1 : {
cont.label = 2; // 更新label
// 這是一個(gè)掛起點(diǎn),我們要保存上下文數(shù)據(jù),這里就保存a的值
int a = 10;
cont.tempA = a; // 保存a的值
// 獲取上一步的結(jié)果,因?yàn)榉盒筒脸枰獜?qiáng)轉(zhuǎn)
String token = (Object)cont.result;
getInfo(token, cont); // 執(zhí)行對應(yīng)的操作
break;
}
case 2 : {
String info = (Object)cont.result; // 獲取上一步的結(jié)果
println(info); // 執(zhí)行對應(yīng)的操作
// 在掛起點(diǎn)之后,恢復(fù)a的值
int a = cont.tempA;
println(a);
return;
}
}
}
我們可以將每個(gè)case理解為一個(gè)狀態(tài),每個(gè)case分支對應(yīng)的語句,理解為一個(gè)Continuation實(shí)現(xiàn)。
上述偽代碼大致描述了協(xié)程的調(diào)度流程:
1 調(diào)用test函數(shù)時(shí),需要傳入一個(gè)Continuation接口,我們會(huì)對它進(jìn)行二次裝飾。
2 裝飾就是根據(jù)函數(shù)具體邏輯,在內(nèi)部添加額外的上下文數(shù)據(jù)和狀態(tài)信息(也就是label)。
3 每個(gè)狀態(tài)對應(yīng)一個(gè)Continuation接口,里面會(huì)執(zhí)行對應(yīng)的業(yè)務(wù)邏輯。
4 每個(gè)狀態(tài)都會(huì): 保存上下文信息 -> 獲取上一個(gè)狀態(tài)的結(jié)果 -> 執(zhí)行本狀態(tài)業(yè)務(wù)邏輯 -> 恢復(fù)上下文信息。
5 直到最后一個(gè)狀態(tài)對應(yīng)的邏輯執(zhí)行完畢。
總結(jié):
1 Kotlin中,每個(gè)suspend方法,都需要一個(gè)Continuation接口實(shí)現(xiàn),用來執(zhí)行下一個(gè)狀態(tài)的操作;并且,每個(gè)suspend方法的調(diào)用點(diǎn)都會(huì)產(chǎn)生一個(gè)掛起點(diǎn)。
2 每個(gè)掛起點(diǎn),都會(huì)產(chǎn)生一個(gè)label,對應(yīng)于狀態(tài)機(jī)的一個(gè)狀態(tài),不同的狀態(tài)之間,通過Continuation來切換。
3 Kotlin協(xié)程會(huì)在每個(gè)掛起點(diǎn)保存當(dāng)前的上下文數(shù)據(jù),并且在掛起點(diǎn)之后進(jìn)行恢復(fù)。這樣,每個(gè)狀態(tài)之間就是相互獨(dú)立的,可以獨(dú)立調(diào)度。
4 協(xié)程的切換,只不過是從一種狀態(tài)切換到另一種狀態(tài),因?yàn)椴煌瑺顟B(tài)是相互獨(dú)立的,所以在合適的時(shí)機(jī),再切換回來也不會(huì)對結(jié)果造成影響。
協(xié)程為什么說開銷很小
(1)協(xié)程調(diào)用跟切換比線程效率高:協(xié)程執(zhí)行效率極高。協(xié)程不需要多線程的鎖機(jī)制,可以不加鎖的訪問全局變量,所以上下文的切換非常快。
(2)協(xié)程占用內(nèi)存少:執(zhí)行協(xié)程只需要極少的棧內(nèi)存(大概是4~5KB),而默認(rèn)情況下,線程棧的大小為1MB。
(3)切換開銷更少:協(xié)程直接操作棧基本沒有內(nèi)核切換的開銷,所以切換開銷比線程少。詳細(xì)說明:
內(nèi)核態(tài)切換 - 線程
因?yàn)?CPU 在內(nèi)核態(tài)切換執(zhí)行單元(線程)時(shí),會(huì)有時(shí)間成本,在進(jìn)行切換執(zhí)行單元時(shí),需要保存寄存器中的數(shù)據(jù),將原執(zhí)行單元的狀態(tài)保存,切換操作也會(huì)占用 CPU 資源(時(shí)間片),從而減少了供線程運(yùn)行的 CPU 資源(時(shí)間片)。用戶態(tài)切換 - 協(xié)程
協(xié)程的切換成本較低,是因?yàn)榍袚Q比較簡單,并且是在用戶態(tài)進(jìn)行切換,切換的時(shí)間成本較低(納秒級(jí)),只需將當(dāng)前協(xié)程的 CPU 寄存器的狀態(tài)先保存起來,然后將需要 CPU 資源的協(xié)程的 CPU 寄存器的狀態(tài)加載到 CPU 寄存器中。
協(xié)程切線程的原理
協(xié)程就是通過Dispatchers調(diào)度器來控制線程切換的。
官方提供了四種實(shí)現(xiàn):Dispatchers.Main,Dispatchers.IO,Dispatchers.Default,Dispatchers.Unconfined。
Dispatchers.Main的實(shí)現(xiàn)
internal class HandlerContext private constructor(
private val handler: Handler,
private val name: String?,
private val invokeImmediately: Boolean
) : HandlerDispatcher(), Delay {
public constructor(
handler: Handler,
name: String? = null
) : this(handler, name, false)
//...
override fun dispatch(context: CoroutineContext, block: Runnable) {
// 利用主線程的 Handler 執(zhí)行任務(wù)
handler.post(block)
}
}
可以看到,其實(shí)就是用handler切換到了主線程。
其他幾個(gè)切換也類似此流程,主要是Dispatcher不同,比如Dispatchers.Default是創(chuàng)建了一個(gè)默認(rèn)的線程池,而線程如果不配置,默認(rèn)是Dispatchers.Default。
而Dispatchers.IO也是沿用的線程池,只是對線程數(shù)量做了限制罷了。
withContext線程切換原理
協(xié)程的線程切換說簡單也很簡單,簡單到一個(gè)設(shè)計(jì)模式就搞定:裝飾器模式。
就是CoroutineContext上下文包裝的分發(fā)器Dispatchers對CoroutineContext的重新裝飾,使其具備不同的Dispatchers能力。
參考:
https://blog.csdn.net/u012165769/article/details/118488207
http://www.lxweimin.com/p/f54b01275228
參考:
https://www.modb.pro/db/211852
https://mp.weixin.qq.com/s/70wBBKwFFLb0X_zrsvNzDA
https://blog.csdn.net/jinking01/article/details/130520579