Kotlin 協(xié)程中,關(guān)于 runBlocking, launch ,withContext ,async,doAsync 之間的簡單區(qū)別

引入大佬的話,Kotlin的協(xié)程,本質(zhì)上是一個線程框架,它可以方便的切換線程的上下文(如主線程切換到子線程/子線程切回主線程)。而平時我們要想在Android Studio使用協(xié)程,先要在gradle引入?yún)f(xié)程依賴:

implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:1.3.3"
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.3'

首先,創(chuàng)建一個協(xié)程的方式有很多種,可以通過 runBlocking,launch (CoroutineScope.lauch / GlobalScope.lauch),withContext ,async 等這些方法來都能創(chuàng)建協(xié)程,這些都是我們可能會在協(xié)程里用到的常見方法。關(guān)于這幾個有什么的區(qū)別,下面給它大概歸為這幾類分別進(jìn)行簡單的對比 :

可在全局創(chuàng)建協(xié)程的: lauch 與 runBlocking

lauch 與 runBlocking都能在全局開啟一個協(xié)程,但 lauch 是非阻塞的runBlocking 是阻塞的

下面通過 lauch 來執(zhí)行一個協(xié)程:

btn.setOnClickListener {
    CoroutineScope(Dispatchers.Main).launch{
        delay(500)     //延時500ms
        Log.e("TAG","1.執(zhí)行CoroutineScope.... [當(dāng)前線程為:${Thread.currentThread().name}]")
    }
    Log.e("TAG","2.BtnClick.... [當(dāng)前線程為:${Thread.currentThread().name}]")
}

E: 2.BtnClick.... [當(dāng)前線程為:main]
E: 1.執(zhí)行CoroutineScope.... [當(dāng)前線程為:main]

從上面運行結(jié)果可以看出,通過CoroutineScope.launch開啟一個協(xié)程,協(xié)程體里的任務(wù)時就會先掛起(suspend),讓CoroutineScope.launch后面的代碼繼續(xù)執(zhí)行,直到協(xié)程體內(nèi)的方法執(zhí)行完成再自動切回來所在的上下文回調(diào)結(jié)果。

CoroutineScope.launch 中我們可以看到接收了一個參數(shù)Dispatchers.Main,這是一個表示協(xié)程上下文的參數(shù),用于指定該協(xié)程體里的代碼運行在哪個線程。當(dāng)指定為Dispatchers.Main時,協(xié)程體里的代碼也是運行在主線程。 當(dāng)指定為Dispatchers.IO,則當(dāng)前協(xié)程運行在一個子線程里。

還有剛提到了掛起 suspend , 它是Kotlin 中的一個關(guān)鍵字,它一般標(biāo)識在一個函數(shù)的開頭,用于表示該函數(shù)是個耗時操作,如上面的delay(timeMillis: Long)就是被聲明為 suspend 函數(shù)。這個關(guān)鍵字主要作用就是為了作一個提醒,并不會因為添加了這個關(guān)鍵字就會該函數(shù)立即跑到一個子線程上。suspend 函數(shù)是只能在協(xié)程體內(nèi)生效,在Kotlin 協(xié)程中,當(dāng)遇到 suspend 函數(shù)的時候 ,該協(xié)程會自動逃離當(dāng)前所在的線程執(zhí)行任務(wù),此時原來協(xié)程所在的線程就繼續(xù)干自己的事,等到協(xié)程的suspend 函數(shù)執(zhí)行完成后又自動切回來原來線程繼續(xù)往下走。一句話說,suspend就是讓協(xié)程該走就走,該回來就回來。 但如果協(xié)程所在的線程已經(jīng)運行結(jié)束了,協(xié)程還沒執(zhí)行完成就不會繼續(xù)執(zhí)行了 。為了避免這樣的情況就需要結(jié)合 runBlocking 來暫時阻塞當(dāng)前線程,保證代碼的執(zhí)行順序。

下面再來看通過 runBlocking 來執(zhí)行一個協(xié)程:

    btn.setOnClickListener {
        runBlocking{
            delay(500)    //延時500ms
            Log.e("TAG","1.runBlocking.... [當(dāng)前線程為:${Thread.currentThread().name}]")
        }
        Log.e("TAG","2.BtnClick.... [當(dāng)前線程為:${Thread.currentThread().name}]")
    }

點擊按鈕后運行結(jié)果:
: 1.runBlocking.... [當(dāng)前線程為:main]
: 2.BtnClick.... [當(dāng)前線程為:main]

運行結(jié)果剛好和上面launch啟動的協(xié)程例子相反,這里的Log先輸出1...再輸出2..., 程序會等待執(zhí)行完成runBlocking內(nèi)的協(xié)程體代碼,再執(zhí)行它后面的代碼。 runBlocking里的任務(wù)如果是非常耗時的操作時,會一直阻塞當(dāng)前線程,在實際開發(fā)中很少會用到runBlocking。 由于runBlocking 接收的 lambda 代表著一個 CoroutineScope,所以 runBlocking 協(xié)程體內(nèi)可繼續(xù)通過launch來繼續(xù)創(chuàng)建一個協(xié)程,避免了lauch所在的線程已經(jīng)運行結(jié)束而切不回來的情況。

可返回結(jié)果的協(xié)程:withContext 與 async

withContext 與 async 都可以返回耗時任務(wù)的執(zhí)行結(jié)果。 一般來說,多個 withContext 任務(wù)是串行的, 且withContext 可直接返回耗時任務(wù)的結(jié)果。 多個 async 任務(wù)是并行的,async 返回的是一個Deferred<T>,需要調(diào)用其await()方法獲取結(jié)果。

使用 withContext 獲取耗時任務(wù)結(jié)果:定義2個耗時任務(wù),一個2000ms后返回結(jié)果,另一個1000ms后返回結(jié)果

btn.setOnClickListener {
    CoroutineScope(Dispatchers.Main).launch {
        val time1 = System.currentTimeMillis()
 
        val task1 = withContext(Dispatchers.IO) {
            delay(2000)
            Log.e("TAG", "1.執(zhí)行task1.... [當(dāng)前線程為:${Thread.currentThread().name}]")
            "one"  //返回結(jié)果賦值給task1
        }
                
        val task2 = withContext(Dispatchers.IO) {
            delay(1000)
            Log.e("TAG", "2.執(zhí)行task2.... [當(dāng)前線程為:${Thread.currentThread().name}]")
            "two"  //返回結(jié)果賦值給task2
        }
 
        Log.e("TAG", "task1 = $task1  , task2 = $task2 , 耗時 ${System.currentTimeMillis()-time1} ms  [當(dāng)前線程為:${Thread.currentThread().name}]")
    }
}

運行結(jié)果:
: 1.執(zhí)行task1.... [當(dāng)前線程為:DefaultDispatcher-worker-1]
: 2.執(zhí)行task2.... [當(dāng)前線程為:DefaultDispatcher-worker-1]
: task1 = one , task2 = two , 耗時 3009 ms [當(dāng)前線程為:main]

從上面結(jié)果可以看出,多個withConext是串行執(zhí)行,如上代碼執(zhí)行順序為先執(zhí)行task1再執(zhí)行task2,共耗時兩個任務(wù)的所需時間的總和。這是因為withConext是個 suspend 函數(shù),當(dāng)運行到 withConext 時所在的協(xié)程就會掛起,直到withConext執(zhí)行完成后再執(zhí)行下面的方法。所以withConext可以用在一個請求結(jié)果依賴另一個請求結(jié)果的這種情況。

如果同時處理多個耗時任務(wù),且這幾個任務(wù)都無相互依賴時,可以使用 async ... await() 來處理,將上面的例子改為 async 來實現(xiàn)如下 :

btn.setOnClickListener {
    CoroutineScope(Dispatchers.Main).launch {
        val time1 = System.currentTimeMillis()
 
        val task1 = async(Dispatchers.IO) {
            delay(2000)
            Log.e("TAG", "1.執(zhí)行task1.... [當(dāng)前線程為:${Thread.currentThread().name}]")
            "one"  //返回結(jié)果賦值給task1
        }
 
        val task2 = async(Dispatchers.IO) {
            delay(1000)
            Log.e("TAG", "2.執(zhí)行task2.... [當(dāng)前線程為:${Thread.currentThread().name}]")
            "two"  //返回結(jié)果賦值給task2
        }
 
        Log.e("TAG", "task1 = ${task1.await()}  , task2 = ${task2.await()} , 耗時 ${System.currentTimeMillis() - time1} ms  [當(dāng)前線程為:${Thread.currentThread().name}]")
    }
}

運行結(jié)果:
: 2.執(zhí)行task2.... [當(dāng)前線程為:DefaultDispatcher-worker-3]
: 1.執(zhí)行task1.... [當(dāng)前線程為:DefaultDispatcher-worker-3]
: task1 = one , task2 = two , 耗時 2037 ms [當(dāng)前線程為:main]

改為用async后,運行結(jié)果耗時明顯比使用withContext更短,且看到與withContext不同的是,task2比task1優(yōu)先執(zhí)行完成 。所以說 async 的任務(wù)都是并行執(zhí)行的。但事實上有一種情況例外,我們把a(bǔ)wait()方法的調(diào)用提前到 async 的后面:

btn.setOnClickListener {
    CoroutineScope(Dispatchers.Main).launch {
        val time1 = System.currentTimeMillis()
 
        val task1 = async(Dispatchers.IO) {
            delay(2000)
            Log.e("TAG", "1.執(zhí)行task1.... [當(dāng)前線程為:${Thread.currentThread().name}]")
            "one"  //返回結(jié)果賦值給task1
        }.await()
 
        val task2 = async(Dispatchers.IO) {
            delay(1000)
            Log.e("TAG", "2.執(zhí)行task2.... [當(dāng)前線程為:${Thread.currentThread().name}]")
            "two"  //返回結(jié)果賦值給task2
        }.await()
 
        Log.e("TAG", "task1 = $task1  , task2 = $task2 , 耗時 ${System.currentTimeMillis() - time1} ms  [當(dāng)前線程為:${Thread.currentThread().name}]")
    }
}

運行結(jié)果:
: 1.執(zhí)行task1.... [當(dāng)前線程為:DefaultDispatcher-worker-1]
: 2.執(zhí)行task2.... [當(dāng)前線程為:DefaultDispatcher-worker-1]
: task1 = one , task2 = two , 耗時 3016 ms [當(dāng)前線程為:main]

原來await() 僅僅被定義為 suspend 函數(shù),因此直接在async 后面使用 await() 就和 withContext 一樣,程序運行到這里就會被掛起直到該函數(shù)執(zhí)行完成才會繼續(xù)執(zhí)行下一個 async 。但事實上await()也不一定導(dǎo)致協(xié)程會被掛起,await() 只有在 async 未執(zhí)行完成返回結(jié)果時,才會掛起協(xié)程。若 async 已經(jīng)有結(jié)果了,await() 則直接獲取其結(jié)果并賦值給變量,此時不會掛起協(xié)程。

anko里的一個擴(kuò)展函數(shù) doAsync

doAsync 拿出來單獨解釋,是因為它容易聯(lián)想到協(xié)程中的 async,實際上又與 async 關(guān)系不大,因為 doAsync并沒有用到協(xié)程庫中的東西。粗略看了下 doAsync 的源碼它的實現(xiàn)都是基于Java的 Future 類進(jìn)行異步處理和通過Handler進(jìn)行線程切換 ,從而封裝的一個擴(kuò)展函數(shù)方便線程切換。

btn.setOnClickListener {
    doAsync {
        Log.e("TAG", " doAsync...   [當(dāng)前線程為:${Thread.currentThread().name}]")
        uiThread {
            Log.e("TAG", " uiThread....   [當(dāng)前線程為:${Thread.currentThread().name}]")
        }
    }
}

運行結(jié)果:
: doAsync... [當(dāng)前線程為:pool-1-thread-1]
: uiThread.... [當(dāng)前線程為:main]

可以看到通過doAsync創(chuàng)建了一個線程池,所有耗時方法可以放在 doAsync 上,等獲取結(jié)果后可以通過 uiThread { } 來切換會主線程。使用 doAsync 進(jìn)行線程切換讓代碼看上去像同步的方式實現(xiàn)異步的請求。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容