Android中對(duì)Kotlin Coroutines(協(xié)程)的理解(二)

(接第一部分)

  • 異常處理

    1.協(xié)程的異常傳遞

    協(xié)程的異常傳播也是遵循了協(xié)程上下文的機(jī)制,除了取消異常(CancellationException)之外。當(dāng)一個(gè)協(xié)程有了異常,如果沒有主動(dòng)捕獲異常,那么異常會(huì)雙向傳播,流程為:

    • 當(dāng)前協(xié)程出現(xiàn)異常
    • cancel 子協(xié)程
    • 等待子協(xié)程 cancel 完成
    • 傳遞給父協(xié)程并循環(huán)前面步驟

    比如:

    fun main ()  = runBlocking {
        val job1 = launch {
            delay(2000)
            println("job finished")
        }
    
        val job2 = launch {
            delay(1000)
            println("job2 finished")
            throw IllegalArgumentException()
        }
    
        delay(3000)
        println("finished")
    }
    

    運(yùn)行結(jié)果:

    job2 finished
    Exception in thread "main" java.lang.IllegalArgumentException
      at com.test.project.newgb.bluetooth.utils.TestKt$main$1$job2$1.invokeSuspend(Test.kt:393)
      ......
    

    job2 1000ms后就發(fā)生了異常,導(dǎo)致job1和父協(xié)程都直接退出

不同根協(xié)程的協(xié)程之間,異常并不會(huì)自動(dòng)傳遞,比如:

fun main ()  = runBlocking {
    val job1 = launch {
        delay(2000)
        println("job finished")
    }

    val job2 = CoroutineScope(Dispatchers.IO).async{
        delay(1000)
        println("job2 finished")
        throw IllegalArgumentException()
        println("new CoroutineScope finished")
    }

    delay(3000)
    println("finished")
}

運(yùn)行結(jié)果:

job2 finished
job finished
finished

CancellationException(取消異常):

CancellationException 會(huì)被 CoroutineExceptionHandler 忽略,但能被 try-catch 捕獲。

fun main ()  = runBlocking {
    val job1 = launch {
        delay(2000)
        println("job finished")
    }

    val job2 = launch {
        delay(1000)
        println("job2 finished")
    }
    job2.cancel() //job2取消息了,其實(shí)并沒有觸發(fā)CancellationException異常
    delay(3000)
    println("finished")
}

運(yùn)行結(jié)果:

job finished
finished

捕捉一下取消異常:

fun main ()  = runBlocking {
    val job1 = launch {
        delay(2000)
        println("job finished")
    }

    val job2 = launch {
        try {
            delay(1000)
            println("job2 finished")
        } catch (e:Exception) {
            e.printStackTrace()
        }
    }
    job2.cancel()

    delay(3000)
    println("finished")
}

運(yùn)行結(jié)果:

job finished
finished

奇怪,并沒有發(fā)生異常,什么原因呢?

因?yàn)榭扇∠膾炱鸷瘮?shù)會(huì)在取消時(shí)拋出CancellationException,上面delay(1000)會(huì)在取消時(shí)拋出CancellationException,但是上面的代碼中 delay(1000)并沒有執(zhí)行,因?yàn)閰f(xié)程還沒有開始執(zhí)行就被 cancel

上面的例子稍加修改:

fun main ()  = runBlocking {
    val job1 = launch {
        delay(2000)
        println("job finished")
    }

    val job2 = launch {
        try {
            delay(1000)
            println("job2 finished")
        } catch (e:Exception) {
            e.printStackTrace()
        }
    }
    delay(500) //延遲500毫秒,讓job2處于delay狀態(tài)
    job2.cancel()

    delay(3000)
    println("finished")
}

運(yùn)行結(jié)果:

kotlinx.coroutines.JobCancellationException: StandaloneCoroutine was cancelled; job=StandaloneCoroutine{Cancelling}@184f6be2
job finished
finished

去掉捕捉異常:

fun main ()  = runBlocking {
    val job1 = launch {
        delay(2000)
        println("job finished")
    }

    val job2 = launch {
        delay(1000)
        println("job2 finished")
    }
    delay(500)//延遲500毫秒,讓job2處于delay狀態(tài)
    job2.cancel()

    delay(3000)
    println("finished")
}

運(yùn)行結(jié)果:

job finished
finished

為什么沒有拋出異常呢?

因?yàn)?code>kotlin的協(xié)程是這樣規(guī)定的:CancellationException這個(gè)異常是被視為正常現(xiàn)象的取消。協(xié)程在內(nèi)部使用 CancellationException 來進(jìn)行取消,所有處理程序都會(huì)忽略這類異常,因此它們僅用作調(diào)試信息的額外來源,這些信息可以用 catch 塊捕獲。

如果不希望協(xié)程內(nèi)的異常向上傳播或影響同級(jí)協(xié)程。可以使用 SupervisorJob

協(xié)程的上下文為SupervisorJob時(shí),該協(xié)程中的異常不會(huì)向外傳播,因此不會(huì)影響其父親/兄弟協(xié)程,也不會(huì)被其兄弟協(xié)程拋出的異常影響

我們常見的 MainScopeviewModelScopelifecycleScope 都是用 SupervisorJob()創(chuàng)建的,所以這些作用域中的子協(xié)程異常不會(huì)導(dǎo)致根協(xié)程退出

正確使用SupervisorJob的方法:

// job1、job2、job3和job4的父Job都是SupervisorJob

val scope = CoroutineScope(SupervisorJob()) 
job1 = scope.launch {...}
job2 = scope.launch {...}

supervisorScope { 
    job3 = launch {...}
    job4 = launch {...}
}

而不是采用launch(SupervisorJob()){...}這種方式(launch生成的協(xié)程的父jobSupervisorJob,其大括號(hào)內(nèi)部的job依然是普通Job)

比如修改一下第一個(gè)例子:

fun main ()  = runBlocking {
    val scope = CoroutineScope(SupervisorJob())

    scope.launch {
        val job1 = scope.launch {
            delay(2000)
            println("job finished")
        }

        val job2 = scope.launch {
            delay(1000)
            println("job2 finished")
            throw IllegalArgumentException()
        }
        delay(2000)
        println("parent job finished")
    }

    delay(3000)
    println("finished")
}

運(yùn)行結(jié)果:

job2 finished
Exception in thread "DefaultDispatcher-worker-3" java.lang.IllegalArgumentException
  at com.test.project.newgb.bluetooth.utils.TestKt$main$1$1$job2$1.invokeSuspend(Test.kt:396)
  ......  
job finished
parent job finished
finished

雖然job2發(fā)生發(fā)異常,但是并沒有影響job1和父協(xié)程

但是如果采用不正確的方式,比如:

fun main() = runBlocking{
    val grandfatherJob = SupervisorJob()
    //創(chuàng)建一個(gè)Job,
    val job = launch(grandfatherJob) {
        //啟動(dòng)一個(gè)子協(xié)程
        val childJob1 = launch {
            println("childJob1 start")
            delay(1000)

            throw IllegalArgumentException()
            println("childJob1 end")
        }

        val childJob2 = launch {
            println("childJob2 start")
            delay(2000)
            println("childJob2 end")
        }
    }

    delay(3000)
    println("end")
}

運(yùn)行結(jié)果:

childJob1 start
childJob2 start
Exception in thread "main" java.lang.IllegalArgumentException
  ...... 
end

可以看出childJob1的異常影響了childJob2,并沒有阻止異常的傳遞,主要就是SupervisorJob的使用方式不對(duì)。

GlobalScope.launch方式啟動(dòng)的是頂層協(xié)程,本身不存在父協(xié)程,在里面發(fā)生異常后, 只會(huì)在logCat輸出異常異常,并不會(huì)影響到外部線程的運(yùn)行,比如:

fun main() = runBlocking {
    println("start")
    GlobalScope.launch {
        println("launch Throwing exception")
        throw NullPointerException()
    }
    Thread.sleep(3000)
    //GlobalScope.launch產(chǎn)生的異常不影響該線程執(zhí)行
    println("end")
}

運(yùn)行結(jié)果:

start
launch Throwing exception
Exception in thread "DefaultDispatcher-worker-1" java.lang.NullPointerException
  at com.test.project.newgb.bluetooth.utils.TestKt$main$1$1.invokeSuspend(Test.kt:511)
  ......
end

再比如:

fun main() = runBlocking {
    println("start")

    launch {
        GlobalScope.launch {
            println("launch Throwing exception")
            throw NullPointerException()
        }
        delay(1000)
        println("out launch end")
    }

    delay(3000)
    //GlobalScope.launch產(chǎn)生的異常不影響該線程執(zhí)行
    println("end")
}

運(yùn)行結(jié)果:

start
launch Throwing exception
Exception in thread "DefaultDispatcher-worker-1" java.lang.NullPointerException
  at com.test.project.newgb.bluetooth.utils.TestKt$main$1$1$1.invokeSuspend(Test.kt:513)
  at kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(ContinuationImpl.kt:33)
  ......
out launch end
end

GlobalScope.async呢?與GlobalScope.launch是不同的,因?yàn)?code>GlobalScope.async在使用await()方法時(shí)會(huì)拋出異常,比如:

fun main() = runBlocking {
    println("start")
    val job = GlobalScope.async {
        println("launch Throwing exception")
        throw NullPointerException()
    }

    job.join()//采用join

    println("end")
}

輸出:

start
launch Throwing exception
end

join改為await

fun main() = runBlocking {
    println("start")
    val job = GlobalScope.async {
        println("launch Throwing exception")
        throw NullPointerException()
    }

    job.await()//采用await

    println("end")
}

輸出:

start
launch Throwing exception
Exception in thread "main" java.lang.NullPointerException
  at com.test.project.newgb.bluetooth.utils.TestKt$main$1$job$1.invokeSuspend(Test.kt:511)
  at kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(ContinuationImpl.kt:33)
  ......

可以看出GlobalScope.async出現(xiàn)異常后,當(dāng)使用了await時(shí),還是會(huì)影響外部線程。

2.協(xié)程的異常處理

launch:

通過launch啟動(dòng)的異常可以通過try-catch來進(jìn)行異常捕獲,或者使用協(xié)程封裝的拓展函數(shù)runCatching來捕獲(其內(nèi)部也是使用的try-catch),還可以使用CoroutineExceptionHandler 對(duì)異常進(jìn)行統(tǒng)一處理,這也更符合結(jié)構(gòu)化并發(fā)原則。

使用try-catch時(shí),要注意:不要用try-catch直接包裹launch、async

使用CoroutineExceptionHandler 捕獲異常需要滿足:

CoroutineExceptionHandler 需要存在于 CoroutineScopeCoroutineContext 中,或者在 CoroutineScope 或者 supervisorScope 創(chuàng)建的直接子協(xié)程中。

采用try-catch的例子:

fun main() = runBlocking {
    val scope = CoroutineScope(Job())

    scope.launch {
        try {
            throw NullPointerException("a exception")
        } catch(e: Exception) {
            println("handle exception : ${e.message}")
        }
    }

    delay(1000)
}

輸出:

handle exception : a exception

or

fun main() = runBlocking {
    val scope = CoroutineScope(Job())

    scope.launch {
        runCatching {
            throw NullPointerException("a exception")
        }.onFailure {
            println("handle exception : ${it.message}")
        }
    }

    delay(1000)
}

輸出:

handle exception : a exception

如果直接用try-catch包裹launch

fun main() = runBlocking {
    val scope = CoroutineScope(Job())

    try {
        scope.launch {
            throw NullPointerException("a exception")
        }
    } catch(e: Exception) {
        println("handle exception : ${e.message}")
    }


    delay(1000)
}

輸出:

Exception in thread "DefaultDispatcher-worker-1" java.lang.NullPointerException: a exception
  at com.test.project.newgb.bluetooth.utils.TestKt$main$1$1.invokeSuspend(Test.kt:530)
  at kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(ContinuationImpl.kt:33)
  at kotlinx.coroutines.DispatchedTask.run(DispatchedTask.kt:106)
  ......

可以發(fā)現(xiàn),異常并沒有被捕獲,所以要將try-catch放到協(xié)程體內(nèi)部

采用CoroutineExceptionHandler的例子:

fun main() = runBlocking {

    val handleException = CoroutineExceptionHandler { _, throwable ->
        println("CoroutineExceptionHandler catch $throwable")
    }

    val scope = CoroutineScope(Job()+handleException)

    scope.launch {
        throw NullPointerException("a exception")
    }

    delay(1000)
}

輸出:

CoroutineExceptionHandler catch java.lang.NullPointerException: a exception

or

fun main() = runBlocking {

    val handleException = CoroutineExceptionHandler { _, throwable ->
        println("CoroutineExceptionHandler catch $throwable")
    }

    val scope = CoroutineScope(Job())

    scope.launch(handleException) {
        throw NullPointerException("a exception")
    }

    delay(1000)
}

輸出:

CoroutineExceptionHandler catch java.lang.NullPointerException: a exception

如果改為這樣呢:

fun main() = runBlocking {

    val handleException = CoroutineExceptionHandler { _, throwable ->
        println("CoroutineExceptionHandler catch $throwable")
    }

    val scope = CoroutineScope(Job())

    scope.launch {

        launch(handleException) {
            throw NullPointerException("a exception")
        }
    }

    delay(1000)
}

輸出:

Exception in thread "DefaultDispatcher-worker-2" java.lang.NullPointerException: a exception
  at com.test.project.newgb.bluetooth.utils.TestKt$main$1$1$1.invokeSuspend(Test.kt:536)
  at kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(ContinuationImpl.kt:33)
  ......

因?yàn)?code>CoroutineExceptionHandler使用的位置不對(duì),所以并沒有發(fā)揮作用

再修改一下:

fun main() = runBlocking {

    val handleException = CoroutineExceptionHandler { _, throwable ->
        println("CoroutineExceptionHandler catch $throwable")
    }

    val scope = CoroutineScope(Job())

    scope.launch(handleException) {

        launch {
            throw NullPointerException("a exception")
        }
    }

    delay(1000)
}

輸出:

CoroutineExceptionHandler catch java.lang.NullPointerException: a exception

所以要注意使用CoroutineExceptionHandler 捕獲異常時(shí)需要滿足的條件。

async:

  • 當(dāng) async 開啟的協(xié)程為根協(xié)程(由協(xié)程作用域直接管理的協(xié)程) 或 supervisorScope 的直接子協(xié)程時(shí),異常不會(huì)主動(dòng)拋出,異常在調(diào)用 await 時(shí)拋出,使用 try-catch 可以捕獲異常

  • 當(dāng) async 開啟的協(xié)程為根協(xié)程(由協(xié)程作用域直接管理的協(xié)程) 或 supervisorScope 的直接子協(xié)程時(shí),如果不調(diào)用await,異常不會(huì)主動(dòng)拋出,同時(shí)產(chǎn)生的異常不影響外部線程執(zhí)行(相當(dāng)于內(nèi)部消化了,但是協(xié)程內(nèi)部異常發(fā)生后的代碼不會(huì)再執(zhí)行)

  • 當(dāng) async 開啟的協(xié)程不為根協(xié)程(不由協(xié)程作用域直接管理的協(xié)程) 同時(shí)也不是supervisorScope 的直接子協(xié)程時(shí),異常發(fā)生時(shí)會(huì)立即拋出,此時(shí)可以用try-catch 或者CoroutineExceptionHandler 捕獲并攔截異常

  • 如果發(fā)生了嵌套,比如多個(gè) asynclaunch嵌套,前面三條仍然成立,即主要看根協(xié)程是否為async所開啟,因?yàn)樽訁f(xié)程的異常會(huì)一直向上傳遞給父協(xié)程,所以要把async 開啟的協(xié)程內(nèi)部看成一個(gè)整體

根協(xié)程(由協(xié)程作用域直接管理的協(xié)程) ,await 時(shí)拋出異常:

比如:

fun main() = runBlocking {
    //async 開啟的協(xié)程為根協(xié)程
    val deferred = GlobalScope.async {
        throw Exception()
    }
    try {
        deferred.await() //拋出異常
    } catch (t: Throwable) {
        println("捕獲異常:$t")
    }
} 

輸出:

捕獲異常:java.lang.Exception

supervisorScope 的直接子協(xié)程,await 時(shí)拋出異常:

fun main() = runBlocking {
    supervisorScope {
        //async 開啟的協(xié)程為 supervisorScope 的直接子協(xié)程
        val deferred = async {
            throw Exception()
        }
        try {
            deferred.await() //拋出異常
        } catch (t: Throwable) {
            println("捕獲異常:$t")
        }
    }
}

輸出:

捕獲異常:java.lang.Exception

根協(xié)程(由協(xié)程作用域直接管理的協(xié)程) ,不用await:

fun main() = runBlocking {
    //async 開啟的協(xié)程為根協(xié)程
    val deferred = GlobalScope.async {
        println("async a coroutine")
        throw Exception()
    }
    try {
        deferred.join()
    } catch (t: Throwable) {
        println("捕獲異常:$t")
    }
    delay(1000)
    println("end")
}

輸出:

async a coroutine
end

上面并沒有捕捉到異常,外部的線程也沒有被影響

supervisorScope 的直接子協(xié)程,不用await

fun main() = runBlocking {
    supervisorScope {
        //async 開啟的協(xié)程為 supervisorScope 的直接子協(xié)程
        val deferred = async {
            println("async a coroutine")
            throw Exception()
        }

        try {
            deferred.join()
        } catch (t: Throwable) {
            println("捕獲異常:$t")
        }
    }
    delay(1000)
    println("end")
}

輸出:

async a coroutine
end

上面并沒有捕捉到異常,外部的線程也沒有被 影響

如果是同一線程呢,比如:

override fun onCreate(savedInstanceState: Bundle?) {
    ......
    test()
    println("ddd test end")
}

fun test() {
    MainScope().launch {
        //async 開啟的協(xié)程為協(xié)程作用域直接管理的協(xié)程
        val deferred = GlobalScope.async(Dispatchers.Main) {
            println("ddd async a coroutine thread:${Thread.currentThread().name}")
            throw Exception()
        }
        try {
            deferred.join()
        } catch (t: Throwable) {
            println("ddd 捕獲異常:$t")
        }
        delay(1000)
        println("ddd end thread:${Thread.currentThread().name}")
    }
}

輸出:

ddd test end
ddd async a coroutine thread:main
ddd end thread:main

可看出async在主線程發(fā)生了異常,但是沒有影響主線程的執(zhí)行,把deferred.join()去掉結(jié)果也一樣

其它情況,異常會(huì)在發(fā)生時(shí)立刻拋出并傳播,需要在異常發(fā)生的地方進(jìn)行捕捉,比如:

fun main() = runBlocking {

    val deferred = async {
        try {
            println("async a coroutine")
            throw Exception()
        } catch (t: Throwable) {
            println("捕獲異常:$t")
        }
    }

    deferred.await() //不能在此捕捉,雖然能捕捉到異常,但是無法阻止異常的傳播

    delay(1000)
    println("end")
}

輸出:

async a coroutine
捕獲異常:java.lang.Exception
end

使用CoroutineExceptionHandler

fun main() = runBlocking {

    val handleException = CoroutineExceptionHandler { _, throwable ->
        println("CoroutineExceptionHandler catch $throwable")
    }

    val scope = CoroutineScope(Job() + handleException )

    val deferred = scope.async {
        println("async a coroutine")
        throw Exception()
    }

    deferred.await() 

    delay(1000)
    println("end")
}

輸出:

async a coroutine
Exception in thread "main" java.lang.Exception
  at com.test.project.newgb.bluetooth.utils.TestKt$main$1$deferred$1.invokeSuspend(Test.kt:627)
  at kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(ContinuationImpl.kt:33)
  ......

奇怪,并沒有捕捉到異常,為什么?可以再回頭看看async的第一條,原來是因?yàn)?code>async產(chǎn)生了頂級(jí)協(xié)程,只能在await時(shí)捕捉,改一下:

fun main() = runBlocking {

    val handleException = CoroutineExceptionHandler { _, throwable ->
        println("CoroutineExceptionHandler catch $throwable")
    }

    val scope = CoroutineScope(Job() + handleException )

    //由于為頂級(jí)協(xié)程,故CoroutineExceptionHandler不會(huì)起作用
    val deferred = scope.async {
        println("async a coroutine")
        throw Exception()
    }

    try {
        deferred.await() //async頂級(jí)協(xié)程需要在此捕捉
    } catch (t: Throwable) {
        println("捕獲異常:$t")
    }


    delay(1000)
    println("end")
}

發(fā)生嵌套的情況

fun main() = runBlocking {

    val handleException = CoroutineExceptionHandler { _, throwable ->
        println("CoroutineExceptionHandler catch $throwable")
    }

    val scope = CoroutineScope(Job() + handleException )

    val deferred = scope.async {
        async {
            launch {
                println("async a coroutine")
                throw Exception()
            }
        }
    }

    //下面的try-catch所有代去掉,將不會(huì)產(chǎn)生異常
    try {
        deferred.await()
    } catch (t: Throwable) {
        println("捕獲異常:$t")
    }


    delay(1000)
    println("end")
}

輸出:

async a coroutine
捕獲異常:java.lang.Exception
end

可以看出CoroutineExceptionHandler并沒有起作用,異常只會(huì)在deferred.await()拋出,同時(shí)只能在此捕獲,原因就是協(xié)程作用域直接管理async,符合第一條

去掉try-catch,驗(yàn)證一下:

fun main() = runBlocking {

    val handleException = CoroutineExceptionHandler { _, throwable ->
        println("CoroutineExceptionHandler catch $throwable")
    }

    val scope = CoroutineScope(Job() + handleException )

    val deferred = scope.async {
        async {
            launch {
                println("async a coroutine")
                throw Exception()
            }
        }
    }
    
    delay(1000)
    println("end")
}

輸出:

async a coroutine
end

此時(shí)并沒有拋出異常

將最外層改為launch

fun main() = runBlocking {

    val handleException = CoroutineExceptionHandler { _, throwable ->
        println("CoroutineExceptionHandler catch $throwable")
    }

    val scope = CoroutineScope(Job() + handleException )

    val job = scope.launch {
        async {
            launch {
                println("async a coroutine")
                throw Exception()
            }
        }
    }

    delay(1000)
    println("end")
}

輸出:

async a coroutine
CoroutineExceptionHandler catch java.lang.Exception
end

可以看出拋出了異常,而且被CoroutineExceptionHandler捕獲

我們說過,要把async下面的看成一個(gè)整體,可以驗(yàn)證一下:

fun main() = runBlocking {

    val handleException = CoroutineExceptionHandler { _, throwable ->
        println("CoroutineExceptionHandler catch $throwable")
    }

    val scope = CoroutineScope(Job() + handleException )

    val deferred = scope.async {
        async {
            val deferred2 = async {
                println("async a coroutine")
                throw Exception()
            }
            println("async in await")
            deferred2.await()  //去掉這條代碼,也一樣不會(huì)拋出異常
            println("async in end") //不會(huì)執(zhí)行,因?yàn)閳?zhí)行前出現(xiàn)異常
        }
        println("async out delay start")
        delay(1000)
        println("async out end")  //不會(huì)執(zhí)行,因?yàn)閳?zhí)行前出現(xiàn)異常
    }

    delay(1000)
    println("end")
}

輸出:

async out delay start
async in await
async a coroutine
end

可以看出并沒有拋出異常,因?yàn)樽罱K要交給最外層的async來處理,里面的子協(xié)程自行處理并沒有用。

但是要注意一點(diǎn),雖然沒有拋出異常,但是異常發(fā)生后,async里面異常發(fā)生點(diǎn)后面的代碼是不會(huì)執(zhí)行的

給最外層asyncawait

fun main() = runBlocking {

    val handleException = CoroutineExceptionHandler { _, throwable ->
        println("CoroutineExceptionHandler catch $throwable")
    }

    val scope = CoroutineScope(Job() + handleException )

    val deferred = scope.async {
        async {
            val deferred2 = async {
                println("async a coroutine")
                throw Exception()
            }
            println("async in await")
            deferred2.await()
            println("async in end")
        }
        println("async out delay start")
        delay(1000)
        println("async out end")
    }

    deferred.await() //加上try-catch后能捕捉并攔截異常

    delay(1000)
    println("end")
}

輸出:

async out delay start
async in await
async a coroutine
Exception in thread "main" java.lang.Exception
  at com.test.project.newgb.bluetooth.utils.TestKt$main$1$deferred$1$1$deferred2$1.invokeSuspend(Test.kt:629)
  at kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(ContinuationImpl.kt:33)
  ......

這符合第一條

三、協(xié)程代碼執(zhí)行順序

上面講到異步代碼能按順序執(zhí)行,同步代碼又可以不按順序執(zhí)行,那協(xié)程代碼的執(zhí)行順序到底是什么規(guī)律呢?這個(gè)問題也是我學(xué)習(xí)協(xié)程時(shí)最納悶的一點(diǎn),搞不清楚什么時(shí)候會(huì)順序執(zhí)行,什么時(shí)候又掛起當(dāng)前的代碼去執(zhí)行其它代碼;什么時(shí)候會(huì)等待函數(shù)執(zhí)行完成,什么時(shí)候又不等待函數(shù)執(zhí)行完成。

要掌握協(xié)程代碼執(zhí)行順序的規(guī)律,必須要明白Suspend function

  • 什么是 Suspend function

    Suspend function是用suspend關(guān)鍵字修飾的函數(shù),suspend function需要在協(xié)程中執(zhí)行,或者在另一個(gè)suspend function內(nèi)

    之所以叫掛起函數(shù),是因?yàn)閽炱鸷瘮?shù)具備掛起恢復(fù)協(xié)程的能力,即掛起函數(shù)可以將其所在的協(xié)程掛起,然后在掛起后能恢復(fù)協(xié)程繼續(xù)執(zhí)行(注意:掛起的是整個(gè)協(xié)程,而不是掛起函數(shù)本身)

    掛起函數(shù)并不一定會(huì)掛起協(xié)程,要掛起協(xié)程需要在它內(nèi)部直接或間接調(diào)用 Kotlin 自帶的 suspend 函數(shù),比如你的掛起函數(shù)里只是print一條日志,并不會(huì)起到掛起的作用,所以suspend關(guān)鍵字只是一個(gè)提醒,提醒協(xié)程框架,函數(shù)里面會(huì)有耗時(shí)操作,如果并沒有,將和普通函數(shù)一樣

    掛起函數(shù)掛起協(xié)程屬于'非阻塞式'掛起,即不會(huì)阻塞協(xié)程所在線程,當(dāng)協(xié)程掛起時(shí),線程會(huì)越過協(xié)程繼續(xù)執(zhí)行

    因?yàn)閰f(xié)程能切換線程,所以協(xié)程被掛起有兩種情況:協(xié)程暫停運(yùn)行、協(xié)程被切換到其它線程上運(yùn)行

    掛起函數(shù)執(zhí)行完畢后,協(xié)程從掛起函數(shù)之后的代碼恢復(fù)執(zhí)行,線程會(huì)切回到協(xié)程之前的線程(除Dispatchers.Unconfined外)

  • 掛起函數(shù)舉例

    override fun onCreate(savedInstanceState: Bundle?) {
        ......
        test()
        println("test end")
    }
    
    fun test() {
        MainScope().launch {
            println("MainScope().launch start thread is:${Thread.currentThread().name}")
    
            delay(5000) //掛起函數(shù),會(huì)將MainScope().launch生成的協(xié)程掛起,后面的代碼5秒后執(zhí)行,UI線程繼續(xù)執(zhí)行
    
            println("launch start thread is:${Thread.currentThread().name}")
            launch {
                println("launch thread1 is:${Thread.currentThread().name}")
    
                withContext(Dispatchers.IO) {
                    println("withContext thread is:${Thread.currentThread().name}")
                    delay(2000)
                }
    
                //上面的IO線程中執(zhí)行時(shí),會(huì)將子協(xié)程掛起,下面的打印要等待IO線程執(zhí)行完成
                println("launch thread3 is:${Thread.currentThread().name}")
            }
    
            println("launch end thread is:${Thread.currentThread().name}")
    
            delay(1000) //再次將MainScope().launch生成的協(xié)程掛起,UI線程繼續(xù)執(zhí)行
    
            println("MainScope().launch end thread is:${Thread.currentThread().name}")
        }
    }
    

    輸出:

    16:02:15.525 : test end
    16:02:15.752 : MainScope().launch start thread is:main
    16:02:20.794 : launch start thread is:main
    16:02:20.796 : launch end thread is:main
    16:02:20.799 : launch thread1 is:main
    16:02:20.805 : withContext thread is:DefaultDispatcher-worker-1
    16:02:21.839 : MainScope().launch end thread is:main
    16:02:22.847 : launch thread3 is:main
    

    上面的例子中,創(chuàng)建了兩個(gè)協(xié)程,MainScope().launch創(chuàng)建的是父協(xié)程,launch在父協(xié)程內(nèi)部創(chuàng)建了一個(gè)子協(xié)程,delay(5000)是一個(gè)掛起函數(shù),它將父協(xié)程整體掛起了5秒,這5秒內(nèi)UI線程不會(huì)被阻塞,但是父協(xié)程內(nèi)部delay(5000)后面的代碼不會(huì)執(zhí)行, 5秒后父協(xié)程恢復(fù)執(zhí)行;

    子協(xié)程withContext(Dispatchers.IO)會(huì)將子協(xié)程切換到子線程中運(yùn)行,可以看到子協(xié)程也被掛起了,等withContext(Dispatchers.IO)里面的代碼執(zhí)行完畢后,才恢復(fù)執(zhí)行

    父協(xié)程中delay(1000) 會(huì)再次將父協(xié)程掛起,說明協(xié)程能被多次掛起與恢復(fù)

    有個(gè)問題:子協(xié)程被掛起了,父協(xié)程會(huì)被掛起嗎?答案是不會(huì),從上面的例子可看出,當(dāng)子協(xié)程執(zhí)行delay(2000)被掛起時(shí),父協(xié)程打印出了ddd MainScope().launch end thread is:main,說明子協(xié)程被掛起時(shí),父協(xié)程會(huì)繼續(xù)運(yùn)行。同理父協(xié)程被掛起也不會(huì)導(dǎo)致子協(xié)程被掛起。

  • 影響代碼順序執(zhí)行的因素

協(xié)程的啟動(dòng)模式

協(xié)程的四種啟動(dòng)模式雖說除了LAZY 之外,其它都是在創(chuàng)建時(shí)立即調(diào)度執(zhí)行,但是協(xié)程內(nèi)部代碼的執(zhí)行一般晚于其外部代碼(多線程下可能會(huì)早于,取決于線程的調(diào)度),比如上面的例子中,test()函數(shù)創(chuàng)建了一個(gè)協(xié)程,其內(nèi)部的代碼執(zhí)行是晚于println("ddd test end")這條的,協(xié)程內(nèi)部的執(zhí)行要等待線程調(diào)度的到來。LAZY 模式就更不用說了,需要顯示調(diào)用才會(huì)開始執(zhí)行內(nèi)部邏輯

Suspend function

掛起函數(shù)能掛起協(xié)程并恢復(fù),所以自然的可以影響程序執(zhí)行順序

awaitjoin函數(shù)

使用Defrred.await()Job.join()都可以使協(xié)程等待其它協(xié)程的執(zhí)行結(jié)果,屬于Suspend function的特例,好處是可以無縫銜接程序的并發(fā)執(zhí)行
Defrred.await()能讓調(diào)用await的協(xié)程掛起并等待Defrred對(duì)象所代表的協(xié)程執(zhí)行完畢后立即恢復(fù)執(zhí)行
Job.join()可以讓子協(xié)程執(zhí)行完畢后父協(xié)程才會(huì)執(zhí)行完畢

也許我們用delay也能實(shí)現(xiàn)想要的順序,但是卻不能實(shí)現(xiàn)無縫銜接,舉例:

private suspend fun intValue1(): Int {
    delay(1000)  //模擬多線程執(zhí)行時(shí)間 1秒
    return 1
}

private suspend fun intValue2(): Int {
    delay(2000) //模擬多線程執(zhí)行時(shí)間 2秒
    return 2
}

fun main() = runBlocking {

    val elapsedTime = measureTimeMillis {
        var value1 = 0
        var value2 = 0
        //下面兩個(gè)協(xié)程也是在并發(fā)執(zhí)行
        async { value1 = intValue1() }
        async { value2 = intValue2() }
        
        delay(3000)

        println("the result is ${value1 + value2}")
    }

    println("the elapsedTime is $elapsedTime")
}

輸出:

the result is 3
the elapsedTime is 3012

上面代碼中,我們大概知道兩個(gè)協(xié)程執(zhí)行的時(shí)間,所以等3秒后肯定能得到正確結(jié)果,2.5秒也可以,但是這種方式問題很大,因?yàn)檫@個(gè)等待的時(shí)間不好把握,等待時(shí)間過長(zhǎng)效率不高,等待時(shí)間過短,有可能子協(xié)程還沒出結(jié)果。如果采用Defrred.await(),就能完美解決,改一下:

private suspend fun intValue1(): Int {
    delay(1000)  //模擬多線程執(zhí)行時(shí)間 1秒
    return 1
}

private suspend fun intValue2(): Int {
    delay(2000) //模擬多線程執(zhí)行時(shí)間 2秒
    return 2
}

fun main() = runBlocking {

    val elapsedTime = measureTimeMillis {
        val value1 = async { intValue1() }
        val value2 = async { intValue2() }

        println("the result is ${value1.await() + value2.await()}")
    }

    println("the elapsedTime is $elapsedTime")
}

輸出:

the result is 3
the elapsedTime is 2022

Job.join()的例子:

fun main() = runBlocking {

    //注意,GlobalScope.launch生成的協(xié)程并不是runBlocking的子協(xié)程
    GlobalScope.launch {
        launch {
            delay(2000)
            println("inner launch1")
        }

        launch {
            delay(1000)
            println("inner launch2")
        }

    }

    println("end")
}

輸出:

end

加上delay

fun main() = runBlocking {
    //注意,GlobalScope.launch生成的協(xié)程并不是runBlocking的子協(xié)程
    GlobalScope.launch {
        launch {
            delay(2000)
            println("inner launch1")
        }

        launch {
            delay(1000)
            println("inner launch2")
        }

    }
    delay(3000)
    println("end")
}

輸出:

inner launch2
inner launch1
end

上面雖然是我們要的結(jié)果,但是這個(gè)delay的時(shí)間不好把握,用join:

fun main() = runBlocking {

    val job = GlobalScope.launch {
        launch {
            delay(2000)
            println("inner launch1")
        }

        launch {
            delay(1000)
            println("inner launch2")
        }

    }

    job.join()
    
    println("end")
}

輸出:

inner launch2
inner launch1
end
  • 代碼執(zhí)行順序規(guī)律

    協(xié)程外部,不考慮協(xié)程內(nèi)部,代碼按常規(guī)順序執(zhí)行

    掛起函數(shù)為偽掛起函數(shù)時(shí),則相當(dāng)于普通函數(shù),起不到掛起函數(shù)的作用

    協(xié)程內(nèi)部,沒有子協(xié)程時(shí),無論是否有掛起函數(shù),有無切換線程,代碼按常規(guī)順序執(zhí)行

    協(xié)程內(nèi)部,有子協(xié)程時(shí),如果父子協(xié)程位于同一線程,則父協(xié)程的掛起函數(shù)掛起時(shí)會(huì)暫停父協(xié)程執(zhí)行,子協(xié)程內(nèi)部代碼開始執(zhí)行,父協(xié)程的恢復(fù)執(zhí)行取決于子協(xié)程是否會(huì)掛起或執(zhí)行完;如果父子協(xié)程是多線程并發(fā),執(zhí)行順序符合一般的多線程運(yùn)行規(guī)律,如果父協(xié)程被掛起,其恢復(fù)取決于父協(xié)程被掛起時(shí)的掛起函數(shù)

舉例:

override fun onCreate(savedInstanceState: Bundle?) {
      ......
      test()
      println("test end")
}

fun test() {
    MainScope().launch {
        val job = GlobalScope.launch(Dispatchers.Main) {
            launch {
                println("inner launch1 start thread:${Thread.currentThread()}")
                var i = 0
                while (i++ <= 5){
                    println("inner launch1 print $i")
                    Thread.sleep(500)
                }
                println("inner launch1 end")
            }

            launch {
                println("inner launch2 start thread:${Thread.currentThread()}")
                var i = 0
                while (i++ <= 5){
                    println("inner launch2 print $i")
                    Thread.sleep(500)
                }
                println("inner launch2 end")
            }


            println("withContext creating")
            
            withContext(Dispatchers.IO) {
                println("withContext thread:${Thread.currentThread().name}")
            }

            println("out launch end thread:${Thread.currentThread().name}")
        }

        job.join()

        println("fun end")
    }
}

輸出:

test end
withContext creating
withContext thread:DefaultDispatcher-worker-1
inner launch1 start thread:Thread[main,5,main]
inner launch1 print 1
inner launch1 print 2
inner launch1 print 3
inner launch1 print 4
inner launch1 print 5
inner launch1 print 6
inner launch1 end
inner launch2 start thread:Thread[main,5,main]
inner launch2 print 1
inner launch2 print 2
inner launch2 print 3
inner launch2 print 4
inner launch2 print 5
inner launch2 print 6
inner launch2 end
out launch end thread:main
fun end

父協(xié)程在執(zhí)行掛起函數(shù)withContext時(shí),子協(xié)程開始運(yùn)行,如果我們將withContext也改在Main線程

 withContext(Dispatchers.IO) {
     println("withContext thread:${Thread.currentThread().name}")
 }
 改為
 withContext(Dispatchers.Main) {
     println("withContext thread:${Thread.currentThread().name}")
 }

結(jié)果為:

test end
withContext creating
withContext thread:main
out launch end thread:main
inner launch1 start thread:Thread[main,5,main]
inner launch1 print 1
inner launch1 print 2
inner launch1 print 3
inner launch1 print 4
inner launch1 print 5
inner launch1 print 6
inner launch1 end
inner launch2 start thread:Thread[main,5,main]
inner launch2 print 1
inner launch2 print 2
inner launch2 print 3
inner launch2 print 4
inner launch2 print 5
inner launch2 print 6
inner launch2 end
fun end

從結(jié)果能看到withContext雖然是掛起函數(shù),但是其里面的執(zhí)行線程沒有變化,并沒有起到掛起的作用

改為多線程:

fun test() {
    MainScope().launch {
        val job = GlobalScope.launch {
            launch {
                println("inner launch1 start thread:${Thread.currentThread()}")
                var i = 0
                while (i++ <= 5){
                    println("inner launch1 print $i")
                    Thread.sleep(500)
                }
                println("inner launch1 end")
            }

            launch {
                println("inner launch2 start thread:${Thread.currentThread()}")
                var i = 0
                while (i++ <= 5){
                    println("inner launch2 print $i")
                    Thread.sleep(500)
                }
                println("inner launch2 end")
            }

            println("withContext creating")
            withContext(Dispatchers.Main) {
                println("withContext thread:${Thread.currentThread().name}")
            }

            println("out launch end thread:${Thread.currentThread().name}")
        }

        job.join()

        println("fun end")
    }
}

輸出:

test end
inner launch1 start thread:Thread[DefaultDispatcher-worker-1,5,main]
inner launch1 print 1
inner launch2 start thread:Thread[DefaultDispatcher-worker-3,5,main]
inner launch2 print 1
withContext creating
withContext thread:main
out launch end thread:DefaultDispatcher-worker-4
inner launch1 print 2
inner launch2 print 2
inner launch1 print 3
inner launch2 print 3
inner launch1 print 4
inner launch2 print 4
inner launch1 print 5
inner launch2 print 5
inner launch1 print 6
inner launch2 print 6
inner launch1 end
inner launch2 end
fun end

可以看出,父子協(xié)程運(yùn)行就是一個(gè)多線程并發(fā)的方式,如果不考慮子協(xié)程,父協(xié)程里的代碼執(zhí)行就是常規(guī)的順序執(zhí)行

四、協(xié)程核心概念

  • CoroutineScope

    協(xié)程作用域(Coroutine Scope)是協(xié)程運(yùn)行的作用范圍,CoroutineScope定義了新啟動(dòng)的協(xié)程作用范圍,同時(shí)會(huì)繼承了他的coroutineContext自動(dòng)傳播其所有的 elements和取消操作。換句話說,如果這個(gè)作用域銷毀了,那么里面的協(xié)程也隨之失效。

    前面說過,全局的GlobalScope是一個(gè)作用域,每個(gè)協(xié)程自身也是一個(gè)作用域,新建的協(xié)程與它的父作用域存在一個(gè)級(jí)聯(lián)的關(guān)系

    驗(yàn)證一下:

    fun main() = runBlocking {
       val job = GlobalScope.launch { //this:CoroutineScope
           
           println("GlobalScope is :$GlobalScope")
           println("GlobalScope.launch's coroutineScope is :$this")
    
           launch { //this:CoroutineScope
               println("launch's coroutineScope is :$this")
    
               launch { //this:CoroutineScope
                   println("launch child's coroutineScope is :$this")
               }
           }
    
           async { //this:CoroutineScope
               println("async's coroutineScope is :$this")
               launch { //this:CoroutineScope
                   println("async child's coroutineScope is :$this")
               }
           }
        }
    
        job.join()
    }
    

    輸出:

    GlobalScope is :kotlinx.coroutines.GlobalScope@781d90ae
    GlobalScope.launch's coroutineScope is :StandaloneCoroutine{Active}@74556741
    launch's coroutineScope is :StandaloneCoroutine{Active}@1cad377c
    launch child's coroutineScope is :StandaloneCoroutine{Active}@41b5b14e
    async's coroutineScope is :DeferredCoroutine{Active}@6a54e1c
    async child's coroutineScope is :StandaloneCoroutine{Active}@3522e255
    

    上面的例子中,所有的協(xié)程都是在GlobalScope之中創(chuàng)建的,但可以看出,里面生成的所有新協(xié)程的CoroutineScope對(duì)象都是新創(chuàng)建 的,全都不同,為什么會(huì)這樣,看源碼就能明白:

    public fun CoroutineScope.launch(
        context: CoroutineContext = EmptyCoroutineContext,
        start: CoroutineStart = CoroutineStart.DEFAULT,
        //block代表的lambda的調(diào)用對(duì)象是CoroutineScope類型
        block: suspend CoroutineScope.() -> Unit
    ): Job {
        val newContext = newCoroutineContext(context)
        val coroutine = if (start.isLazy)
            LazyStandaloneCoroutine(newContext, block) else
            StandaloneCoroutine(newContext, active = true)
        
        //block傳到了start函數(shù)之中
        coroutine.start(start, coroutine, block) 
        return coroutine
    }
    
    //block的調(diào)用對(duì)象變?yōu)榱薘類型,而R類型是receiver表示的參數(shù)確定的,receiver的實(shí)參是launch函數(shù)中的coroutine變量
    //其為L(zhǎng)azyStandaloneCoroutine或者StandaloneCoroutine對(duì)象
    public fun <R> start(start: CoroutineStart, receiver: R, block: suspend R.() -> T) {
            startAbstractCoroutine(start, receiver, this, block)
    }
    

    從源碼得知,launch函數(shù)調(diào)用時(shí) {}所包含的代碼塊的調(diào)用主體最終變成了LazyStandaloneCoroutineStandaloneCoroutine對(duì)象(這兩類型的父類型也是CoroutineScope),這個(gè)對(duì)象是在launch中新創(chuàng)建的。

    async函數(shù)也類似。

    雖然不同,但是其CoroutineContext變量會(huì)進(jìn)行傳遞,保證了協(xié)程的結(jié)構(gòu)化并發(fā)特征。為什么這樣做,我的理解是CoroutineScope只是一個(gè)接口,只包含一個(gè)變量,太過于簡(jiǎn)單,為了攜帶更多信息,所以要進(jìn)行轉(zhuǎn)換。

  • Job

    一個(gè)Job是對(duì)一個(gè)協(xié)程的句柄。你創(chuàng)建的每個(gè)協(xié)程,不管你是通過launch還是async來啟動(dòng)的,它都會(huì)返回一個(gè)Job實(shí)例,唯一標(biāo)識(shí)該協(xié)程,并可以通過該Job管理其生命周期。在CoroutineScope的構(gòu)造函數(shù)中也傳入了一個(gè)Job,可以保持對(duì)其生命周期的控制。

    Job 的生命周期由狀態(tài)表示,下面是其狀態(tài)機(jī)圖示:

“Active” 狀態(tài)下,一個(gè) Job 正在運(yùn)行并執(zhí)行它的工作。如果 Job 是通過協(xié)程構(gòu)建器創(chuàng)建的,這個(gè)狀態(tài)就是協(xié)程主體運(yùn)行時(shí)的狀態(tài)。在這種狀態(tài)下,我們可以啟動(dòng)子協(xié)程。大多數(shù)協(xié)程會(huì)在 “Active” 狀態(tài)下啟動(dòng)。只有那些延遲啟動(dòng)的才會(huì)以 “New” 狀態(tài)啟動(dòng)。當(dāng)它完成時(shí)候,它的狀態(tài)變?yōu)?“Completing”,等待所有子協(xié)程完成。一旦它的所有子協(xié)程任務(wù)都完成了,其狀態(tài)就會(huì)變?yōu)?“Completed”,這是一個(gè)最終狀態(tài)。或者,如果 Job 在運(yùn)行時(shí)候(在 “Active” 或者 “Completing” 狀態(tài)下)取消或失敗,其狀態(tài)將會(huì)改變成為 “Cancelling”。在這種狀態(tài)下,最后還可以做一些清理工作,比如關(guān)閉連接或釋放資源。完成此操作后, Job 將會(huì)進(jìn)入到 “Cancelled” 狀態(tài)。

Job存在父子關(guān)系,比如:

val grandfatherJob = SupervisorJob()
//創(chuàng)建一個(gè)Job,
val job = GlobalScope.launch(grandfatherJob) {
    //啟動(dòng)一個(gè)子協(xié)程
    val childJob = launch {
    }
}

上面的代碼中有三個(gè)JobgrandfatherJob、job、childJob,其中job父親為grandfatherJobchildJob父親為job

增加打印語句,來印證一下:

fun main() = runBlocking{
    val grandfatherJob = SupervisorJob()
    //創(chuàng)建一個(gè)Job,
    val job = GlobalScope.launch(grandfatherJob) {
        println("job start")
        //啟動(dòng)一個(gè)子協(xié)程
        val childJob = launch {
            println("childJob start")
        }
        println("job end")
    }

    println("job's child is ${job.children.elementAtOrNull(0)}")

    println("grandfatherJob's child is ${grandfatherJob.children.elementAtOrNull(0)}")

    println("end")
}

輸出:

job start
job end
childJob start
job's child is null
grandfatherJob's child is null
end

上面不是說:job父親為grandfatherJobchildJob父親為job,為什么打印出來jobgrandfatherJob的子協(xié)程都為空呢?

主要是如果子協(xié)程如果執(zhí)行完了,會(huì)自動(dòng)從children這個(gè)Sequence中清除掉,如果我們?cè)诖蛴?code>child時(shí),讓子協(xié)程還在運(yùn)行中:

fun main() = runBlocking{
    val grandfatherJob = SupervisorJob()
    //創(chuàng)建一個(gè)Job,
    val job = GlobalScope.launch(grandfatherJob) {
        println("job start")
        //啟動(dòng)一個(gè)子協(xié)程
        val childJob = launch {
            println("childJob start")
            delay(1000)  //延遲1秒
        }

        delay(2000) //延遲2秒
        println("job end")
    }

    println("job's child is ${job.children.elementAtOrNull(0)}")

    println("grandfatherJob's child is ${grandfatherJob.children.elementAtOrNull(0)}")

    println("end")
}

結(jié)果如下:

job start
childJob start
job's child is StandaloneCoroutine{Active}@59e5ddf
grandfatherJob's child is StandaloneCoroutine{Active}@536aaa8d
end

運(yùn)行結(jié)果與預(yù)想一致。

Job的父子關(guān)系如何建立:

協(xié)程構(gòu)建器基于其父 Job 構(gòu)建其 Job

每個(gè)協(xié)程構(gòu)建器都會(huì)創(chuàng)建其它們自己的 Job,大多數(shù)協(xié)程構(gòu)建器會(huì)返回 Job

Job 是唯一一個(gè)不是子協(xié)程直接繼承父協(xié)程的上下文(上下文即CoroutineContextJob也是繼承自CoroutineContext)。每個(gè)協(xié)程都會(huì)創(chuàng)建自己的 Job,來自傳遞參數(shù)或者父協(xié)程的 Job 將會(huì)被用作這個(gè)子協(xié)程所創(chuàng)建 Job 的父 Job,比如:

fun main() = runBlocking {
    val name = CoroutineName("Some name")
    val job = Job()

    launch(name + job) {
        val childName = coroutineContext[CoroutineName]
        println(childName == name) // true
        //childJob是在launch中新建的Job,但其與”val job = Job()“中的job保持著父子關(guān)系
        val childJob = coroutineContext[Job] 
        println(childJob == job) // false
        println(childJob == job.children.first()) // true
    }
}

如果新的 Job 上下文取代了父 Job 的上下文,結(jié)構(gòu)化并發(fā)機(jī)制將不起作用,比如:

fun main(): Unit = runBlocking {
    launch(Job()) { // 使用新 Job 取代了來自父協(xié)程的 Job
        delay(1000)
        println("Will not be printed")
    }
}
// (不會(huì)打印任何東西,程序會(huì)馬上結(jié)束))

在上面的例子中,父協(xié)程將不會(huì)等待子協(xié)程,因?yàn)樗c子協(xié)程沒有建立關(guān)系,因?yàn)樽訁f(xié)程使用來自參數(shù)的 Job 作為父 Job,因此它與 runBlockingJob 沒有關(guān)系。

下面再用兩段程序作說明:

private fun test1() {
    //總共有5個(gè)Job:SupervisorJob、newJob、Job0、Job1、Job2
    val scope = MainScope() //SupervisorJob(無子Job)
    
    //Job()會(huì)生成newJob,scope.launch會(huì)生成Job0,而Job0的父Job是newJob,Job0的子Job是Job1、Job2
    scope.launch(Job()) {  //此處使用新 Job 取代了來自父協(xié)程的 Job
        launch { //Job1
            delay(2000L)
            println("CancelJobActivity job1 finished")
            scope.cancel()
        }
        launch { //Job2
            delay(3000L)
            println("CancelJobActivity job2 finished") //會(huì)輸出
        }
    }
}

private fun test2() {
    //總共有4個(gè)Job:SupervisorJob、Job0、Job1、Job2
    val scope = MainScope()//SupervisorJob(子Job為Job0)
    scope.launch { //Job0(子Job為Job1、Job2)
        launch { //Job1
            delay(2000L)
            println("CancelJobActivity job1 finished")
            scope.cancel()

        }
        launch { //Job2
            delay(3000L)
            println("CancelJobActivity job2 finished") //不會(huì)輸出
        }
    }
}

Job 使用join 方法用來等待,直到所有協(xié)程完成。這是一個(gè)掛起函數(shù),它掛起直到每個(gè)具體的子 Job 達(dá)到最終狀態(tài)(Completed 或者 Cancelled)。比如:

fun main(): Unit = runBlocking {
    val job1 = launch {
        delay(1000)
        println("Test1")
    }
    val job2 = launch {
        delay(2000)
        println("Test2")
    }
    job1.join()
    job2.join()
    println("All tests are done")
}

輸出:

Test1
Test2
All tests are done

上面例子中,可以看到Job 接口還暴露了一個(gè) children 屬性,允許我們?cè)L問它的所有子 job,比如:

fun main(): Unit = runBlocking {
    launch {
        delay(1000)
        println("Test1")
    }
    launch {
        delay(2000)
        println("Test2")
    }
    
    val children = coroutineContext[Job]
        ?.children
    val childrenNum = children?.count()
    println("Number of children: $childrenNum")
    children?.forEach { it.join() }
    println("All tests are done")
}

輸出:

Number of children: 2
Test1
Test2
All tests are done

理解:join()調(diào)用在哪個(gè)協(xié)程之中,則這個(gè)協(xié)程的結(jié)束需要等待調(diào)用join函數(shù)的Job結(jié)束。上面的例子,join()runBlocking之中被調(diào)用,所以runBlocking要結(jié)束,需要等待job1、job2先結(jié)束。

  • CoroutineContext

    CoroutineContext管理了協(xié)程的生命周期,線程調(diào)度,異常處理等功能,在創(chuàng)建協(xié)程時(shí),都會(huì)新建一個(gè)CoroutineContext對(duì)象,該對(duì)象可以手動(dòng)創(chuàng)建傳入或者會(huì)自動(dòng)創(chuàng)建一個(gè)默認(rèn)值

    CoroutineContext是一個(gè)特殊的集合,既有Map的特點(diǎn),也有Set的特點(diǎn),集合的每一個(gè)元素都是Element,每個(gè)Element都有一個(gè)Key與之對(duì)應(yīng),對(duì)于相同KeyElement是不可以重復(fù)存在的,Element之間可以通過 + 號(hào)組合起來

    CoroutineContext包含了如下Element元素:

    Job:協(xié)程的唯一標(biāo)識(shí),用來控制協(xié)程的生命周期(new、active、completing、completed、cancelling、cancelled),默認(rèn)為null,比如GlobalScope中的Jobnull;

    CoroutineDispatcher:指定協(xié)程運(yùn)行的線程(IO、Default、Main、Unconfined),默認(rèn)為Default;

    CoroutineName:協(xié)程的名稱,調(diào)試的時(shí)候很有用,默認(rèn)為coroutine;

    CoroutineExceptionHandler:指定協(xié)程的異常處理器,用來處理未捕獲的異常.

    Element元素對(duì)應(yīng)的Key,可以直接用Element元素本身,比如要從CoroutineContext中獲取Job元素的值,通過CoroutineContext[Job]即可,之所以能這能這樣,在于Kotlin有一個(gè)特性:一個(gè)類的名字本身就可以作為其伴生對(duì)象的引用,所以coroutineContext[Job]只是coroutineContext[Job.Key]的一個(gè)簡(jiǎn)寫方式,實(shí)際上最原始的寫法應(yīng)該是這樣:coroutineContext[object : CoroutineContext.Key<Job> {}]Job的源碼部分片斷:

    public interface Job : CoroutineContext.Element {
        
        public companion object Key : CoroutineContext.Key<Job>
        
        ......
    }    
    

    上面的伴生對(duì)象Key,可以直接用Job來替代。

    實(shí)際上Job、CoroutineDispatcher、CoroutineName、CoroutineExceptionHandler都是CoroutineContext類型,因?yàn)樗鼈兌祭^承自CoroutineContext.Element,而CoroutineContext.Element繼承自CoroutineContext。既然Element是元素,CoroutineContext是集合,為什么元素本身也是集合呢,為什么要這樣設(shè)計(jì)?主要是為了API設(shè)計(jì)方便,能非常方便的實(shí)現(xiàn) +操作。

    對(duì)于新創(chuàng)建的協(xié)程,它的CoroutineContext會(huì)包含一個(gè)全新的Job實(shí)例,它會(huì)幫助我們控制協(xié)程的生命周期,而剩下的元素會(huì)從CoroutineContext的父類繼承,該父類可能是另外一個(gè)協(xié)程或者創(chuàng)建該協(xié)程的CoroutineScope

    fun main()  {
        val b = runBlocking {
            println("Level 0 Job:${coroutineContext[Job]}")
    
            val scope = CoroutineScope(Job()+Dispatchers.IO+CoroutineName("test"))
    
            println("Level 1 Job:${scope.coroutineContext[Job]}")
    
            val job= scope.launch {
                //新的協(xié)程會(huì)將CoroutineScope作為父級(jí)
                println("Level 2 Job:${coroutineContext[Job]}")
                println("Level 2 CoroutineName:${coroutineContext[CoroutineName]}")
                println("Level 2 CoroutineDispatcher:${coroutineContext[CoroutineDispatcher]}")
                println("Level 2 CoroutineExceptionHandler:${coroutineContext[CoroutineExceptionHandler]}")
                println("Level 2 Thread:${Thread.currentThread().name}")
    
                val result = async {
                    //通過async創(chuàng)建的新協(xié)程會(huì)將當(dāng)前協(xié)程作為父級(jí)
                    println("Level 3 Job:${coroutineContext[Job]}")
                    println("Level 3 CoroutineName:${coroutineContext[CoroutineName]}")
                    println("Level 3 CoroutineDispatcher:${coroutineContext[CoroutineDispatcher]}")
                    println("Level 3 Thread:${Thread.currentThread().name}")
                }.await()
            }
            job.join()
        }
    }
    

    輸出:

    Level 0 Job:BlockingCoroutine{Active}@5f9d02cb
    Level 1 Job:JobImpl{Active}@3a5ed7a6
    Level 2 Job:StandaloneCoroutine{Active}@2f3b4d1b
    Level 2 CoroutineName:CoroutineName(test)
    Level 2 CoroutineDispatcher:Dispatchers.IO
    Level 2 CoroutineExceptionHandler:null
    Level 2 Thread:DefaultDispatcher-worker-1
    Level 3 Job:DeferredCoroutine{Active}@74a4da96
    Level 3 CoroutineName:CoroutineName(test)
    Level 3 CoroutineDispatcher:Dispatchers.IO
    Level 3 Thread:DefaultDispatcher-worker-3
    

    CoroutineContext生成規(guī)則:

    新創(chuàng)建的CoroutineContext = 默認(rèn)值 + 繼承的CoroutineContext +參數(shù)

    元素不指定的話,會(huì)給出默認(rèn)值,比如:CoroutineDispatcher默認(rèn)值為Dispatchers.DefaultCoroutineName默認(rèn)值為"coroutine"

    繼承的CoroutineContextCoroutineScope或者其父協(xié)程的CoroutineContext

    傳入?yún)f(xié)程構(gòu)建器的參數(shù)的優(yōu)先級(jí)高于繼承的上下文參數(shù),因此會(huì)覆蓋對(duì)應(yīng)的參數(shù)值

    子協(xié)程繼承父協(xié)程時(shí),除了Job會(huì)自動(dòng)創(chuàng)建新的實(shí)例外,其他3項(xiàng)的不手動(dòng)指定的話,都會(huì)自動(dòng)繼承父協(xié)程的

五、協(xié)程原理

一個(gè)問題:能不能實(shí)現(xiàn)一個(gè)java庫,實(shí)現(xiàn)類似kotlin協(xié)程的功能?

我認(rèn)為理論上是可以的,但肯定沒協(xié)程這般優(yōu)雅。因?yàn)樾枰猛降姆绞綄懗霎惒綀?zhí)行的代碼,所以代碼肯定需要在編譯前進(jìn)行處理,可以參考一下Butterknife中用到的APT(注解處理器),APT能根據(jù)注解自動(dòng)生成代碼。掌握了協(xié)程的原理,能更好的回答這個(gè)問題。

  • 狀態(tài)機(jī)是什么

    表示有限個(gè)狀態(tài)以及在這些狀態(tài)之間的轉(zhuǎn)移和動(dòng)作等行為的數(shù)學(xué)模型,簡(jiǎn)單點(diǎn)說就是用幾個(gè)狀態(tài)來代表和控制執(zhí)行流程。一般同一時(shí)間點(diǎn)只有一種狀態(tài),每種狀態(tài)對(duì)應(yīng)一種處理邏輯,處理后轉(zhuǎn)為下一個(gè)狀態(tài),比如可以用Switch(kotlin里用when語句)來實(shí)現(xiàn)狀態(tài)機(jī):

    class StateMachine {
        var state = 0
    }
    
    fun main() {
        val stateMachine = StateMachine()
        repeat(3) {
            when (stateMachine.state){
                0 -> {
                    println("狀態(tài)0做事")
                    stateMachine.state = 1
                }
                1 -> {
                    println("狀態(tài)1做事")
                    stateMachine.state = 33
                }
                else -> {
                    println("其他狀態(tài)做事")
                }
            }
        }
    }
    

    輸出:

    狀態(tài)0做事
    狀態(tài)1做事
    其他狀態(tài)做事
    
  • 回調(diào)函數(shù)+狀態(tài)機(jī)

    比如實(shí)現(xiàn)一個(gè)簡(jiǎn)單的回調(diào)函數(shù)+狀態(tài)機(jī):

    interface Callback {
        fun callback()
    }
    
    fun myFunction(Callback: Callback?) {
        class MyFunctionStateMachine : Callback {
            var state = 0
            override fun callback() {
                myFunction(this)//調(diào)用本函數(shù)
            }
        }
    
        val machine = if (Callback == null) MyFunctionStateMachine()
        else Callback as MyFunctionStateMachine
        when (machine.state) {
            0 -> {
                println("狀態(tài)0做事")
                machine.state = 1
                machine.callback()
            }
            1 -> {
                println("狀態(tài)1做事")
                machine.state = 33
                machine.callback()
            }
            else -> {
                println("其他狀態(tài)做事")
                machine.state = 0
                machine.callback()
            }
        }
    }
    
    fun main() {
        myFunction(null)
    }
    

    輸出:

    狀態(tài)0做事
    狀態(tài)1做事
    其他狀態(tài)做事
    狀態(tài)0做事
    狀態(tài)1做事
    其他狀態(tài)做事
    ......
    

    會(huì)一直循環(huán)輸出:

    狀態(tài)0做事
    狀態(tài)1做事
    其他狀態(tài)做事

  • suspend 函數(shù)的真面目

    編譯器會(huì)將suspend函數(shù)變成一個(gè)"回調(diào)函數(shù)+狀態(tài)機(jī)"的模式,函數(shù)定義的轉(zhuǎn)變,比如:

    suspend fun getUserInfo(): String {
        ......
    }
    
    變?yōu)椋?
    public static final Object getUserInfo(@NotNull Continuation var0) {
        ......
    }
    
    suspend fun getFriendList(user: String): String {
        ......
    }
    
    變?yōu)椋? 
    public static final Object getFriendList(@NotNull String var0, @NotNull Continuation var1) {
        ......
    }
    

    可以看到suspend函數(shù)參數(shù)中增加了一個(gè)類型為Continuation的參數(shù),Kotlin Compiler使用Continuation參數(shù)代替了suspend修飾符,看看Continuation的定義:

    public interface Continuation<in T> {
        public val context: CoroutineContext
    
        public fun resumeWith(result: Result<T>)
    }
    

    Continuation類型變量就是充當(dāng)了回調(diào)函數(shù)的角色,這個(gè)從掛起函數(shù)轉(zhuǎn)換成CallBack函數(shù)的過程,被稱為:CPS 轉(zhuǎn)換(Continuation-Passing-Style Transformation),即續(xù)體傳遞風(fēng)格變換,我們可以稱Continuation為一個(gè)續(xù)體

    函數(shù)的返回類型也會(huì)變?yōu)?code>Any?(表現(xiàn)在Java字節(jié)碼中則為Object),這是因?yàn)槿绻?code>suspend函數(shù)里面調(diào)用了delay之類的函數(shù)導(dǎo)致suspended發(fā)生的話,函數(shù)會(huì)返回一個(gè)enum類型:COROUTINE_SUSPENDED,所以需要用Any?作返回類型

看看函數(shù)體的轉(zhuǎn)變:

suspend fun getUserInfo(): String { 
    delay(5000L)
    
    return "BoyCoder"
}

變?yōu)椋?
public static final Object getUserInfo(@NotNull Continuation var0) {
    Object $continuation;
    label20: {
         if (var0 instanceof GetUserInfoMachine) {
            $continuation = (GetUserInfoMachine)var0;
            if (($continuation).label & Integer.MIN_VALUE) != 0) {
               $continuation).label -= Integer.MIN_VALUE;
               break label20;
            }
         }

         $continuation = new GetUserInfoMachine(var0);    
    }
    
    Object $result = $continuation).result; 
    Object var3 = IntrinsicsKt.getCOROUTINE_SUSPENDED();
    switch($continuation).label) {   
      case 0:
         ResultKt.throwOnFailure($result);
         $continuation.label = 1;
         if (DelayKt.delay(5000L, (Continuation)$continuation) == var3) {
            return var3;
         }
         break;
      case 1:
         ResultKt.throwOnFailure($result);
         break;
      default:
         throw new IllegalStateException("call to 'resume' before 'invoke' with coroutine");  
    }
     
    return "BoyCoder";
}

static final class GetUserInfoMachine extends ContinuationImpl {
    Object result;
    int label;

    GetUserInfoMachine(Continuation $completion) {
        super($completion);
    }

    @Nullable
    public final Object invokeSuspend(@NotNull Object $result) {
        this.result = $result;
        this.label |= Integer.MIN_VALUE;
        return TestKt.getUserInfo(null, (Continuation<? super String>) this);
    }
}

GetUserInfoMachine的繼承關(guān)系如下:

GetUserInfoMachine -> ContinuationImpl -> BaseContinuationImpl -> Continuation

internal abstract class BaseContinuationImpl(
    public val completion: Continuation<Any?>?
) : Continuation<Any?>, CoroutineStackFrame, Serializable {
    // This implementation is final. This fact is used to unroll resumeWith recursion.
    public final override fun resumeWith(result: Result<Any?>) {
        // This loop unrolls recursion in current.resumeWith(param) to make saner and shorter stack traces on resume
        var current = this
        var param = result
        while (true) {
            // 在每個(gè)恢復(fù)的continuation進(jìn)行調(diào)試探測(cè),使得調(diào)試庫可以精確跟蹤掛起的調(diào)用棧中哪些部分
            // 已經(jīng)恢復(fù)了。
            probeCoroutineResumed(current)
            with(current) {
                val completion = completion!! // fail fast when trying to resume continuation without completion
                val outcome: Result<Any?> =
                    try {
                        val outcome = invokeSuspend(param)
                        if (outcome === COROUTINE_SUSPENDED) return
                        Result.success(outcome)
                    } catch (exception: Throwable) {
                        Result.failure(exception)
                    }
                releaseIntercepted() // this state machine instance is terminating
                if (completion is BaseContinuationImpl) {
                    // unrolling recursion via loop
                    current = completion
                    param = outcome
                } else {
                    // top-level completion reached -- invoke and return
                    completion.resumeWith(outcome)
                    return
                }
            }
        }
    }

    protected abstract fun invokeSuspend(result: Result<Any?>): Any?

    protected open fun releaseIntercepted() {
        // does nothing here, overridden in ContinuationImpl
    }    
    
    ...
}

delay的定義:

public static final Object delay(long timeMillis, @NotNull Continuation $completion) {
    if (timeMillis <= 0L) {
        return Unit.INSTANCE;
    } else {
        // 實(shí)現(xiàn)類
        CancellableContinuationImpl cancellableContinuationImpl = new CancellableContinuationImpl(IntrinsicsKt.intercepted($completion), 1);
        cancellableContinuationImpl.initCancellability();
        // 向上轉(zhuǎn)型
        CancellableContinuation cont = (CancellableContinuation)cancellableContinuationImpl;
        if (timeMillis < Long.MAX_VALUE) {
            // 延時(shí)操作
            getDelay(cont.getContext()).scheduleResumeAfterDelay(timeMillis, cont);
        }
      // 獲取執(zhí)行結(jié)果
        Object result = cancellableContinuationImpl.getResult();
        if (result == COROUTINE_SUSPENDED) {
            DebugProbesKt.probeCoroutineSuspended($completion);
        }
      // 返回結(jié)果
        return result;
    }
}

getUserInfo函數(shù)執(zhí)行的過程:

1.調(diào)用getUserInfo函數(shù),傳入var0(即自動(dòng)產(chǎn)生的續(xù)體對(duì)象,Continuation類型),其不為GetUserInfoMachine類型,所以new了一個(gè)GetUserInfoMachine對(duì)象,并將var0保存到GetUserInfoMachine對(duì)象中,同時(shí)將GetUserInfoMachine對(duì)象賦給$continuation變量

2.由于$continuation.label = 0,執(zhí)行case 0分支

3.case 0分支中將$continuation.label置為1,調(diào)用DelayKt.delay方法

4.執(zhí)行delay方法,$continuation傳入到delay中(保存在變量$completion中,協(xié)程恢復(fù)時(shí)會(huì)用到),delay返回COROUTINE_SUSPENDED,表示掛起

5.case 0中,直接return 結(jié)果 ,最后getUserInfo函數(shù)返回COROUTINE_SUSPENDED

6.因?yàn)?code>getUserInfo函數(shù)已返回COROUTINE_SUSPENDEDgetUserInfo函數(shù)暫時(shí)執(zhí)行完畢,線程執(zhí)行其它動(dòng)作(通過暫時(shí)結(jié)束方法調(diào)用的方式,讓協(xié)程暫時(shí)不在這個(gè)線程上面執(zhí)行,線程可以去處理其它的任務(wù),協(xié)程的掛起就不會(huì)阻塞當(dāng)前的線程,這就是為什么協(xié)程能非阻塞式掛起)

7.目前getUserInfo函數(shù)所在的協(xié)程處于掛起狀態(tài),而delay函數(shù)會(huì)在某個(gè)子線程執(zhí)行等待操作(這也是為什么我們的suspend函數(shù)一定要調(diào)用系統(tǒng)的suspend函數(shù)的原因,系統(tǒng)的函數(shù)才有這個(gè)能力),等延時(shí)時(shí)間到達(dá)之后,就會(huì)調(diào)用傳給delay函數(shù)的$completionresumeWith方法,也就是調(diào)用GetUserInfoMachineresumeWith方法,即BaseContinuationImplresumeWith方法,來進(jìn)行協(xié)程的恢復(fù)

8.BaseContinuationImplresumeWith方法會(huì)調(diào)用到GetUserInfoMachine對(duì)象的invokeSuspend方法

9.invokeSuspend方法中,又開始調(diào)用getUserInfo函數(shù),傳入var0參數(shù),此時(shí)var0為之前創(chuàng)建的GetUserInfoMachine對(duì)象

10.由于$continuation.label = 1,執(zhí)行case 1分支

11.最后getUserInfo函數(shù)執(zhí)行結(jié)束并返回了"BoyCoder"

12.此時(shí)回到第8步的BaseContinuationImplresumeWith方法中,invokeSuspend執(zhí)行的結(jié)果即是第11步返回的"BoyCoder",保存到了outcome變量中

13.resumeWith方法接著執(zhí)行Result.success(outcome),并將結(jié)果保存到外部的val outcome: Result<Any?> ,這個(gè)變量中

14.completion變量(此變量就是第1步中傳入的var0)此時(shí)不為BaseContinuationImpl,最后會(huì)執(zhí)行completion.resumeWith(outcome),表示結(jié)束,我們可以看看源碼里的注釋:// top-level completion reached -- invoke and return翻譯過來就是:達(dá)到頂級(jí)完成--調(diào)用并返回

說明一下,如果getUserInfo函數(shù)調(diào)用的不是delay,而是另一個(gè)有返回值的suspend函數(shù),就會(huì)執(zhí)行if (completion is BaseContinuationImpl) {}里的代碼,形成一種遞歸調(diào)用

另外我們說過掛起函數(shù)掛起的是整個(gè)協(xié)程,不是掛起函數(shù)本身,所以第6步里getUserInfo函數(shù)返回COROUTINE_SUSPENDED后,協(xié)程里面,getUserInfo掛起函數(shù)后面的代碼暫時(shí)是不會(huì)執(zhí)行的,協(xié)程本身也是一個(gè)狀態(tài)機(jī),整個(gè)協(xié)程也暫時(shí)會(huì)返回COROUTINE_SUSPENDED,所以協(xié)程才會(huì)因?yàn)閽炱鸷瘮?shù)而掛起

  • 協(xié)程的真面目

    上面只是舉了一個(gè)最簡(jiǎn)單的例子,能作為分析協(xié)程原理的一個(gè)基礎(chǔ)。

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

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