Kotlin協(xié)程原理

線程與協(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

https://www.bilibili.com/video/BV1KJ41137E9/?spm_id_from=333.337.search-card.all.click&vd_source=40c24e77b23dc2e50de2b7c87c6fed59

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

推薦閱讀更多精彩內(nèi)容