Kotlin學習 9 -- 協(xié)程

本篇文章主要介紹以下幾個知識點:

SUMMER DAY (圖片來源于網(wǎng)絡(luò))

1. 協(xié)程的基本用法

協(xié)程 可以簡單看作是一種輕量級的線程翎碑。一般線程需要依賴操作系統(tǒng)的調(diào)度才能實現(xiàn)不同線程之間的切換百宇,而協(xié)程卻可以在編程語言層面實現(xiàn)不同協(xié)程之間的切換他嫡,大大提升了并發(fā)編程的運行效率哈踱。

使用協(xié)程需要添加如下依賴:

implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:1.3.0"
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.0"

1.1 GlobalScope.launch 函數(shù)與 runBlocking 函數(shù)

開啟一個協(xié)程最簡單的方式是用 GlobalScope.launch 函數(shù)传黄,如下:

fun main() {
    // GlobalScope.launch 函數(shù)可創(chuàng)建一個協(xié)程作用域鬓长,里面的代碼塊就是在協(xié)程中運行了
    GlobalScope.launch {
        println("code run in coroutine scope")
    }
}

當然,上面運行 main() 函數(shù)并不能打印出日志尝江,因為 GlobalScope.launch 函數(shù)每次創(chuàng)建都是一個頂層協(xié)程涉波,這種協(xié)程當程序運行結(jié)束時也會跟著一起結(jié)束,使得應(yīng)用程序結(jié)束了而上面代碼塊中的代碼還沒來得及運行炭序。

解決這個問題很簡單啤覆,讓程序延遲一段時間在結(jié)束運行即可:

fun main() {
    // GlobalScope.launch 函數(shù)可創(chuàng)建一個協(xié)程作用域,里面的代碼塊就是在協(xié)程中運行了
    GlobalScope.launch {
        println("code run in coroutine scope")
    }
    Thread.sleep(1000)
}

上面代碼運行可以正常打印日志了惭聂,但若代碼塊中的代碼不能在延遲一段時間內(nèi)運行結(jié)束窗声,那么還是會存在問題:

fun main() {
    GlobalScope.launch {
        println("code run in coroutine scope")
        // delay() 是一個非阻塞式掛起函數(shù),它只會掛起當前協(xié)程辜纲,不影響其他協(xié)程運行
        // delay() 只能在協(xié)程的作用域或其他掛起函數(shù)中調(diào)用
        // 這邊讓協(xié)程掛起1.5秒
        delay(1500) 
        println("code run in coroutine scope finished")
    }
    // 和 delay() 函數(shù)不同笨觅,Thread.sleep() 會阻塞當前的線程,運行在該線程下的所有協(xié)程都會被阻塞
    Thread.sleep(1000)
}

上面運行會發(fā)現(xiàn)第二條日志沒打印出來耕腾,原因還是因為它還沒來得及運行應(yīng)用程序就結(jié)束了见剩。

當然,此時可以借助 runBlocking 函數(shù)了扫俺,它同樣會創(chuàng)建一個協(xié)程作用域苍苞,但它可以保證在協(xié)程作用域內(nèi)的所有代碼和子協(xié)程全部執(zhí)行完之前一直阻塞當前線程:

fun main() {
    runBlocking {
        println("code run in coroutine scope")
        delay(1500)
        println("code run in coroutine scope finished")
    }
}

這樣,兩條日志都可以正常打印出來了狼纬。

注:runBlocking 函數(shù)通常只在測試環(huán)境下使用羹呵,在正式環(huán)境中使用容易產(chǎn)生一些性能上的問題。

1.2 launch 函數(shù)

當涉及到高并發(fā)的應(yīng)用場景時疗琉,就能體現(xiàn)出協(xié)程相比線程的優(yōu)勢冈欢,創(chuàng)建多個協(xié)程可以使用 launch 函數(shù):

fun main() {
    runBlocking { 
        // launch 函數(shù)必須在協(xié)程的作用域才能調(diào)用,它會在當前作用域下創(chuàng)建子協(xié)程
        // 若外層作用域的協(xié)程結(jié)束了盈简,該作用域下的所有子協(xié)程也會一同結(jié)束
        launch { 
            println("launch1")
            delay(1000)
            println("launch1 finished")
        }
        launch {
            println("launch2")
            delay(1000)
            println("launch2 finished")
        }
    }
}
// 打印結(jié)果:launch1凑耻,launch2犯戏,launch1 finished,launch2 finished

上面兩個子協(xié)程的日志是交替打印的拳话,即并發(fā)運行的先匪,不過它們運行在同一個線程中,只是由編程語言來決定如何在多個協(xié)程之間進行調(diào)度弃衍,使得協(xié)程的并發(fā)效率很高呀非。

1.3 suspend 關(guān)鍵字與 coroutineScope 函數(shù)

launch 函數(shù)中編寫的代碼是擁有協(xié)程作用域的,若將部分代碼提取到一個單獨的函數(shù)中就沒有協(xié)程作用域了镜盯,從而無法調(diào)用像 delay() 這樣的掛起函數(shù)岸裙,此時就需要借助 suspend 關(guān)鍵字。

關(guān)鍵字 suspend 可以將任意函數(shù)聲明成掛起函數(shù)速缆,掛起函數(shù)之間可以相互調(diào)用降允,使用如下:

// suspend 關(guān)鍵字只能將一個函數(shù)聲明成掛起函數(shù),是無法給它提供協(xié)程作用域的
// 由于 launch 函數(shù)必須在協(xié)程作用域才能調(diào)用艺糜,因而在這里此時是無法調(diào)用 launch 函數(shù)的
suspend fun printDot() {
    println(".")
    delay(1000)
}

上面要在 printDot() 中調(diào)用 launch 函數(shù)剧董,還需要借助 coroutineScope 函數(shù)。

函數(shù) coroutineScope 是一個掛起函數(shù)破停,從而可以在其他掛起函數(shù)中調(diào)用翅楼,它會繼承外部的協(xié)程作用域并創(chuàng)建一個子作用域,寫法如下:

suspend fun printDot() = coroutineScope {
    launch {
        println(".")
        delay(1000)
    }
}

另外真慢,coroutineScope 函數(shù)和 runBlocking 函數(shù)有點類似毅臊,可以保證其作用域內(nèi)的所有代碼和子協(xié)程在全部執(zhí)行完之前,會一直阻塞當前協(xié)程黑界。


小結(jié)1:coroutineScope 函數(shù)只會阻塞當前協(xié)程管嬉,不影響其他協(xié)程和線程,從而不會有性能上的問題朗鸠;runBlocking 函數(shù)會阻塞當前線程蚯撩,若在主線程中調(diào)用它的話,可能會導致界面卡死的現(xiàn)象童社,不推薦在實際項目中使用求厕。

小結(jié)2: GlobalScope.launchrunBlocking函數(shù)可以在任意地方調(diào)用著隆,coroutineScope 函數(shù)可以在協(xié)程作用域或掛起函數(shù)中調(diào)用扰楼,而 launch 函數(shù)只能在協(xié)程作用域中調(diào)用。

小結(jié)3: GlobalScope.launch 每次創(chuàng)建的都是頂層協(xié)程美浦,一般也不太建議使用弦赖,除非明確要創(chuàng)建頂層協(xié)程。


2. 作用域構(gòu)建器

前面學習的 GlobalScope.launch浦辨、runBlocking蹬竖、launchcoroutineScope 幾種作用域構(gòu)建器,都可以用于創(chuàng)建一個新的協(xié)程作用域币厕。

2.1 取消協(xié)程

不管是 GlobalScope.launch 還是 launch 函數(shù)列另,都會返回一個 Job 對象,調(diào)用其 cancel() 方法可取消協(xié)程旦装,如下:

val job = GlobalScope.launch { 
    // to do something
}
job.cancel()

若在 Acitivity 使用 GlobalScope.launch 這種協(xié)程作用域構(gòu)建器页衙,每次創(chuàng)建的都是頂層協(xié)程,那么當 Activity 關(guān)閉時阴绢,此時需要取消協(xié)程店乐,就需要逐個調(diào)用所有已創(chuàng)建協(xié)程的 cancel() 方法,使得代碼不好維護呻袭,因而在實際項目中不太常用這種協(xié)程作用域構(gòu)建器眨八,比較常用的寫法如下:

// 創(chuàng)建 Job 對象
val job = Job()
// 傳入 CoroutineScope 函數(shù)
val scope = CoroutineScope(job)
scope.launch { 
    // to do something
}
job.cancel()

這樣,所有調(diào)用 CoroutineScopelaunch 函數(shù)創(chuàng)建的協(xié)程都會被關(guān)聯(lián)到 Job 對象的作用域下左电,只需調(diào)用一次 cancel() 方法廉侧,就可把同一作用域內(nèi)的所有協(xié)程取消,大大降低了協(xié)程管理成本篓足。

2.2 async 函數(shù)

調(diào)用 launch 函數(shù)可以創(chuàng)建一個新的協(xié)程伏穆,但它只能用于執(zhí)行一段邏輯,返回一個 Job 對象纷纫,不能獲取執(zhí)行結(jié)果枕扫,要獲取執(zhí)行結(jié)果可以借助 async 函數(shù)。

async 函數(shù)必須在協(xié)程作用域中才能調(diào)用辱魁,它會創(chuàng)建一個新的子協(xié)程并返回一個 Deferred 對象烟瞧,調(diào)用 Deferred 對象的 await() 方法可獲取執(zhí)行結(jié)果,如下:

fun main() {
    runBlocking {
        val result = async {
            10 + 10
        }.await()
        println(result)
    }
}
// 打印結(jié)果:20

當調(diào)用 await() 方法時染簇,若代碼塊中的代碼還沒執(zhí)行完参滴,await() 方法就會將當前協(xié)程阻塞,直到可以獲得 async 函數(shù)的執(zhí)行結(jié)果锻弓。

舉個栗子砾赔,用兩個 async 函數(shù)來執(zhí)行延遲任務(wù),并記錄運行耗時青灼,如下:

fun main() {
    runBlocking {
        val start = System.currentTimeMillis()
        val result1 = async {
            delay(1000)
            10 + 10
        }.await()
        val result2 = async {
            delay(1000)
            15 + 5
        }.await()
        println("result is ${result1 + result2}")
        val end = System.currentTimeMillis()
        println("cost time is ${end - start} ms")
    }
}
// 打印結(jié)果:result is 40暴心,cost time is 2069 ms

由于上面兩個 async 函數(shù)是一種串行的關(guān)系,前一個執(zhí)行完后一個才執(zhí)行杂拨,因而這種寫法非常低效专普,修改代碼使得兩個 async 函數(shù)同時執(zhí)行如下:

fun main() {
    runBlocking {
        val start = System.currentTimeMillis()
        val deferred1 = async {
            delay(1000)
            10 + 10
        }
        val deferred2 = async {
            delay(1000)
            15 + 5
        }
        println("result is ${deferred1.await() + deferred2.await()}")
        val end = System.currentTimeMillis()
        println("cost time is ${end - start} ms")
    }
}
// 打印結(jié)果:result is 40,cost time is 1043 ms

上面在需要用到 async 函數(shù)的執(zhí)行結(jié)果時才調(diào)用 await() 方法進行獲取弹沽,此時就變成一種并行關(guān)系了檀夹,運行效率也就提升了筋粗。

2.3 withContext 函數(shù)

withContext() 函數(shù) 是一個比較特殊的作用域構(gòu)建器,它是一個掛起函數(shù)炸渡,可以理解成 async 函數(shù)的一種簡化版寫法娜亿,用法如下:

fun main() {
    runBlocking {
        // 調(diào)用 withContext() 函數(shù)后,會立即執(zhí)行代碼塊中的代碼蚌堵,同時把當前協(xié)程阻塞
        // 當代碼塊中的代碼執(zhí)行完后會吧最后一行的執(zhí)行結(jié)果作為返回值返回
        // 相當于 val result = async{10 + 10}.await() 的寫法
        val result = withContext(Dispatchers.Default) {
            10 + 10
        }
        println(result)
    }
}

值得注意的是暇唾,withContext() 函數(shù)強制要求指定一個線程參數(shù),線程參數(shù)有以下3種:

  • Dispatchers.Default 使用一種默認低并發(fā)的線程策略

  • Dispatchers.IO 使用一種較高并發(fā)的線程策略辰斋,如網(wǎng)絡(luò)請求

  • Dispatchers.Main 不會開啟子線程策州,在主線程中執(zhí)行,只能在Android項目中用

注:在協(xié)程作用域構(gòu)建器中宫仗,所有的函數(shù)都可以指定一個線程參數(shù)够挂,只不過 withContext() 函數(shù)是強制要求指定的,其他函數(shù)則是可選的藕夫。

3. 使用協(xié)程簡化回調(diào)的寫法

平時網(wǎng)絡(luò)請求數(shù)據(jù)時會采用回調(diào)機制來處理孽糖,回調(diào)機制基本上是靠匿名類來實現(xiàn)的:

val address = "https://www.baidu.com/"
// 模擬一個網(wǎng)絡(luò)請求回調(diào)
HttpUtil.sendHttpRequest(address, object :HttpCallbackListener{
    override fun onFinish(response: String) {
        // 得到服務(wù)器返回內(nèi)容
    }

    override fun onError(e: Exception) {
        // 對異常處理
    }
})

在 Kotlin 中,可以通過 suspendCoroutine 函數(shù)把傳統(tǒng)的回調(diào)機制的寫法大幅簡化毅贮。

suspendCoroutine 函數(shù)必須在協(xié)程作用域或掛起函數(shù)中調(diào)用办悟,它接收一個 Lambda 表達式參數(shù),將當前協(xié)程立即掛起滩褥,然后在一個普通的線程中執(zhí)行 Lambda 表達式中的代碼病蛉。

上述 Lambda 參數(shù)列表上會傳入一個 Continuation 參數(shù),調(diào)用它的 resume() 方法或 resumeWithException() 可以讓協(xié)程恢復執(zhí)行瑰煎。

定義個 request() 函數(shù)優(yōu)化上面的的回調(diào)寫法如下:

suspend fun request(address: String): String {
    return suspendCoroutine { continuation ->
        HttpUtil.sendHttpRequest(address, object : HttpCallbackListener {
            override fun onFinish(response: String) {
                // 成功
                continuation.resume(response)
            }

            override fun onError(e: Exception) {
                // 失敗
                continuation.resumeWithException(e)
            }
        })
    }
}

調(diào)用時就可以這樣寫:

suspend fun getBaiduResponse() {
    try {
        val response = request("https://www.baidu.com/")
        // 進行數(shù)據(jù)處理
    } catch (e: Exception) {
        // 異常處理
    }
}

這樣铺然,代碼就清爽了許多。


再舉個栗子酒甸,平時使用 Retrofit 來發(fā)起網(wǎng)絡(luò)請求時先定義接口和創(chuàng)建構(gòu)建器:

// Retrofit 構(gòu)建器
object ServiceCreator {
    private const val BASE_URL = "..."

    private val retrofit =
        Retrofit.Builder().baseUrl(BASE_URL).addConverterFactory(GsonConverterFactory.create())
            .build()

    // 獲取 Service 接口的動態(tài)代理對象方法
    // 使用:val appService = ServiceCreator.create(AppService::class.java)
    fun <T> create(serviceClass: Class<T>): T = retrofit.create(serviceClass)

    // 通過泛型實化進一步優(yōu)化:獲取 Service 接口的動態(tài)代理對象方法
    // 使用:val appService = ServiceCreator.create<AppService>()
    inline fun <reified T> create(): T = create(T::class.java)
}

// 定義接口
interface ApiService {
    @GET("xxx/xxx")
    fun getAppData(): Call<List<String>>
}

接著發(fā)起網(wǎng)絡(luò)請求:

val appService = ServiceCreator.create<ApiService>()
appService.getAppData().enqueue(object : Callback<List<String>> {
    override fun onFailure(call: Call<List<String>>, t: Throwable) {
        // 失敗邏輯處理
    }

    override fun onResponse(call: Call<List<String>>, response: Response<List<String>>) {
        // 成功邏輯處理
    }
})

這時魄健,使用 suspendCoroutine 函數(shù)可以對上面寫法進行優(yōu)化。使用泛型的方式定義一個 await() 函數(shù)如下:

// 這里 await() 定義成一個掛起函數(shù)插勤,并定義成 Call<T> 的擴展函數(shù)沽瘦,
// 從而所有返回值是 Call 類型的 Retrofit 網(wǎng)絡(luò)請求接口都可以直接調(diào)用 await() 函數(shù)
suspend fun <T> Call<T>.await(): T {
    return suspendCoroutine { continuation ->
        // 由于擴展函數(shù)的原因,這里擁有了 Call 對象的上下文农尖,
        // 從而可以直接調(diào)用 enqueue() 方法讓 Retrofit 發(fā)起網(wǎng)絡(luò)請求
        enqueue(object : Callback<T> {
            override fun onFailure(call: Call<T>, t: Throwable) {
                // 失敗
                continuation.resumeWithException(t)
            }

            override fun onResponse(call: Call<T>, response: Response<T>) {
                // 成功
                val body = response.body()
                if (body != null) continuation.resume(body)
                else continuation.resumeWithException(RuntimeException("body is null"))
            }
        })
    }
}

有了 await() 函數(shù)后析恋,調(diào)用 Retrofit 的接口就會簡單很多,比如上面的發(fā)起網(wǎng)絡(luò)請求就可以寫成:

suspend fun getAppData(){
    try {
        // 只需簡單調(diào)用 await() 函數(shù)即可獲取響應(yīng)數(shù)據(jù)
        val result = ServiceCreator.create<ApiService>().getAppData().await()
        // 成功邏輯處理
    } catch (e: Exception){
        // 異常邏輯處理
    }
}

注:每次發(fā)起請求時都進行 try catch 處理比較麻煩卤橄,可以選擇不處理绿满。在不處理情況下,若發(fā)生異常就會一層層向上拋出窟扑,直到某一層的函數(shù)處理為止喇颁。也可以在某個統(tǒng)一的入口函數(shù)中只進行一次 try catch,從而讓代碼更簡潔嚎货。

本篇文章就介紹到這橘霎。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市殖属,隨后出現(xiàn)的幾起案子姐叁,更是在濱河造成了極大的恐慌,老刑警劉巖洗显,帶你破解...
    沈念sama閱讀 211,042評論 6 490
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件外潜,死亡現(xiàn)場離奇詭異,居然都是意外死亡挠唆,警方通過查閱死者的電腦和手機处窥,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 89,996評論 2 384
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來玄组,“玉大人滔驾,你說我怎么就攤上這事《矶铮” “怎么了哆致?”我有些...
    開封第一講書人閱讀 156,674評論 0 345
  • 文/不壞的土叔 我叫張陵,是天一觀的道長患膛。 經(jīng)常有香客問我摊阀,道長,這世上最難降的妖魔是什么踪蹬? 我笑而不...
    開封第一講書人閱讀 56,340評論 1 283
  • 正文 為了忘掉前任驹溃,我火速辦了婚禮,結(jié)果婚禮上延曙,老公的妹妹穿的比我還像新娘豌鹤。我一直安慰自己,他們只是感情好枝缔,可當我...
    茶點故事閱讀 65,404評論 5 384
  • 文/花漫 我一把揭開白布布疙。 她就那樣靜靜地躺著,像睡著了一般愿卸。 火紅的嫁衣襯著肌膚如雪灵临。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,749評論 1 289
  • 那天趴荸,我揣著相機與錄音儒溉,去河邊找鬼。 笑死发钝,一個胖子當著我的面吹牛顿涣,可吹牛的內(nèi)容都是我干的波闹。 我是一名探鬼主播,決...
    沈念sama閱讀 38,902評論 3 405
  • 文/蒼蘭香墨 我猛地睜開眼涛碑,長吁一口氣:“原來是場噩夢啊……” “哼精堕!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起蒲障,我...
    開封第一講書人閱讀 37,662評論 0 266
  • 序言:老撾萬榮一對情侶失蹤歹篓,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后揉阎,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體庄撮,經(jīng)...
    沈念sama閱讀 44,110評論 1 303
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,451評論 2 325
  • 正文 我和宋清朗相戀三年毙籽,在試婚紗的時候發(fā)現(xiàn)自己被綠了洞斯。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 38,577評論 1 340
  • 序言:一個原本活蹦亂跳的男人離奇死亡惧财,死狀恐怖巡扇,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情垮衷,我是刑警寧澤厅翔,帶...
    沈念sama閱讀 34,258評論 4 328
  • 正文 年R本政府宣布,位于F島的核電站搀突,受9級特大地震影響刀闷,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜仰迁,卻給世界環(huán)境...
    茶點故事閱讀 39,848評論 3 312
  • 文/蒙蒙 一甸昏、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧徐许,春花似錦施蜜、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,726評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至恰起,卻和暖如春修械,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背检盼。 一陣腳步聲響...
    開封第一講書人閱讀 31,952評論 1 264
  • 我被黑心中介騙來泰國打工肯污, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 46,271評論 2 360
  • 正文 我出身青樓蹦渣,卻偏偏與公主長得像哄芜,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子剂桥,可洞房花燭夜當晚...
    茶點故事閱讀 43,452評論 2 348