关于长时间做过Java开发的同伴,协程或许是一个比较生疏的概念,由于现阶段运用Java开发Android应用是无法运用协程的,所以在转到Kotlin开发之后,协程是一个必须要了解的概念,它能够处理传统Android开发中的一些痛点问题,并且现在Google官方现已将Kotlin作为第一开发语言,信任体验到Kotlin的魅力之后就会爱不释手了。

1 初识协程

其实关于协程,坊间议论纷纷,并没有一个统一的说法,并且去找各类博客都不相同,每个人都有每个人的了解,假如同伴们们看到这篇文章,那么就请不要再去看其他材料啦,信任这会是全网比较官方的文档了,并且有助于同伴们更好地了解协程。

首要咱们先创立一个协程,如下:

/**
 * 这是一个顶层函数,用来测试运用
 * 类似于Java中的 static main 函数
 */
fun main(){
    //创立一个协程
    GlobalScope.launch {
        delay(1000)
        println("World!")
    }
    println("Hello,")
}

咱们先不关心GlobalScope是什么,经过调用launch办法就创立了一个协程,然后咱们看在协程里加了一个延迟函数,随后打印一行字符串。

所以当协程运转在主线程之后,其实相当于在主线程中“开辟一个子线程”,此刻主线程会首要打印“Hello,”,然后比及“子线程”履行完毕之后,在打印“World!”,假如按照上面的写法,会如咱们所想的那样吗?

运转之后,咱们发现只打印了一行,并没有打印出“World!”,

Hello,

这是什么原因呢?这儿就涉及到了挂起与康复,当履行到协程代码块中,调用delay函数之后,当时协程会被挂起,假如主线程履行完结那么就直接return,相当于这个进程现已完毕了,其实就没有康复的机会了,假如实际开发中,app主进程当然不会履行完成果退出了,这仅仅一个示例,所以需求加一个堵塞主线程的办法就能够了。

fun main(){
    //创立一个协程
    GlobalScope.launch {
        delay(1000)
        println("World!")
    }
    println("Hello,")
    // 堵塞2s主线程,确保JVM还存活
    Thread.sleep(2000)
}

咱们在打印协程中线程称号时,发现是运转在DefaultDispatcher-worker-1线程中的,而不是main,所以官方中关于协程的界说是“轻量级的线程”,那么已然都叫做线程了,为什么不必Java的Thread呢?到底和Java的线程有什么区别,咱们稍后会着重介绍。

1.1 协程的挂起与堵塞

前面咱们提到了当履行delay函数时,协程会被挂起,那么协程中碰到什么样的场景会被挂起呢,咱们先看下delay函数的源码。

public suspend fun delay(timeMillis: Long) {
    if (timeMillis <= 0) return // don't delay
    return suspendCancellableCoroutine sc@ { cont: CancellableContinuation<Unit> ->
        // if timeMillis == Long.MAX_VALUE then just wait forever like awaitCancellation, don't schedule.
        if (timeMillis < Long.MAX_VALUE) {
            cont.context.delay.scheduleResumeAfterDelay(timeMillis, cont)
        }
    }
}

在这儿,咱们看到一个办法关键字suspend,中文含义便是挂起,所以同伴们记住一点,当协程中履行suspend办法时就会被挂起。那么挂起和堵塞有什么区别呢?由于从用户视角来看现象是共同的。

thread {
    Thread.sleep(1000)
    Log.e("TAG","World!")
}
Log.e("TAG","Hello,")
Thread.sleep(2000)

其实从体系维度来看,两者的不同仍是很大的,像经过Thread.sleep,Thread.wait这种办法是会堵塞线程,直到被唤醒才会持续往下履行,这个进程是经过体系来调度的,会开释CPU资源让其他线程占用。

而协程的挂起是不会开释CPU资源的,也便是说协程的挂起更像是由程序自动建议的,不再交由体系调度,所以咱们能够这样了解,当协程挂起时,会开释底层的线程去干其他作业,这样能够最大限度地运用线程干更多的事,有利于进步功率

所以咱们能够经过一个比如,来验证咱们的定论:发动10w个线程和协程。

val startTime = System.currentTimeMillis()
repeat(100000){
    thread {
        Thread.sleep(100)
    }
}
Log.e("TAG","start 10w thread cost ${System.currentTimeMillis() - startTime}")
2023-06-21 23:17:17.273 16989-16989/com.lay.nowinandroid E/TAG: start 10w thread cost 27279
val startTime = System.currentTimeMillis()
repeat(100000){
    GlobalScope.launch {
        delay(1000)
    }
}
Log.e("TAG","start 10w coroutines cost ${System.currentTimeMillis() - startTime}")
2023-06-21 23:18:53.189 26327-26327/com.lay.nowinandroid E/TAG: start 10w coroutines cost 1088

想必同伴们现已看到了这个巨大的距离了吧,当发动10w个线程时,每发动一次都会堵塞100ms,此刻体系CPU会进行调度,由于在子线程中所以关于主线程并不会形成太大的影响,可是总共用了27279ms完结;

当发动10w线程时,每次创立一次协程都会挂起协程1000ms,总共花费1088ms,其实经过这个比如就能够阐明,当挂起协程时,的确能够进步程序的运转功率。

1.2 协程效果域的构建

由于咱们知道,协程是不会堵塞线程的,线程则是会堵塞线程,那么怎么经过协程的办法来堵塞线程呢?其实Kotlin中供给了相应的api,例如runBlocking、coroutineScope两种效果域构建器,能够完结对线程的堵塞,那么两种构建器有什么特性呢,咱们详细介绍一下。

首要咱们先了解一个概念,协程效果域其实便是供给协程创立的环境,一切的协程都必须要在协程效果域中创立,并且每个协程效果域都会有自己的上下文,每个协程也有自己的上下文,这个在后续文章中会介绍。

1.2.1 runBlocking协程效果域

其实在Kotlin中供给了runBlocking来包装主线程,那么此刻主线程就会一向堵塞到runBlocking中的协程悉数履行完毕,其实便是类似于于Thead.sleep办法。

runBlocking {
    Log.e("TAG","进入到主协程代码块")
    delay(2000)
    Log.e("TAG","进入到主协程代码块finish")
}
Log.e("TAG","进入到主协程代码块履行完毕")
2023-06-21 23:53:19.821 32099-32099/com.lay.nowinandroid E/TAG: 进入到主协程代码块
2023-06-21 23:53:21.854 32099-32099/com.lay.nowinandroid E/TAG: 进入到主协程代码块finish
2023-06-21 23:53:21.855 32099-32099/com.lay.nowinandroid E/TAG: 进入到主协程代码块履行完毕

可是咱们再看一个比如,已然runBlocking协程构建器总是会堵塞到内部每个协程都履行完结,咱们再创立一个协程,看下效果。

runBlocking {
    Log.e("TAG","进入到主协程代码块")
    GlobalScope.launch {
        delay(1000)
        Log.e("TAG","发动一个新的协程finish")
    }
    Log.e("TAG","进入到主协程代码块finish")
}
Log.e("TAG","进入到主协程代码块履行完毕")

咱们经过GlobalScope创立一个协程,抱负状况下咱们认为其内部协程履行完结之后,才会打印“进入到主协程代码块履行完毕”,实际上并不是这样的。

2023-06-21 23:55:42.509 32476-32476/com.lay.nowinandroid E/TAG: 进入到主协程代码块
2023-06-21 23:55:42.529 32476-32476/com.lay.nowinandroid E/TAG: 进入到主协程代码块finish
2023-06-21 23:55:42.530 32476-32476/com.lay.nowinandroid E/TAG: 进入到主协程代码块履行完毕
2023-06-21 23:55:43.576 32476-32510/com.lay.nowinandroid E/TAG: 发动一个新的协程finish

由于经过GlobalScope创立的协程,其生命周期与当时应用程序的生命周期绑定,当时应用程序的生命周期完毕之后,其协程效果域就退出了,因而它并不会保活当时父协程,即runBlocking协程效果域,所以就像咱们上面看到的那样,虽然作为runBlocking协程效果域的子协程,但并没有比及其履行完结再退出。

所以咱们纠正一下前面的说法,关于runBlocking协程效果域来说,并不是只需创立一个协程就能一向堵塞到其履行完结,而是在runBlocking效果域下创立的协程才干一向堵塞到其履行完毕

这句话比较拗口,其实咱们经过一个比如来阐明:

runBlocking {
    Log.e("TAG","进入到主协程代码块")
    this.launch {
        delay(1000)
        Log.e("TAG","发动一个新的协程finish")
    }
    Log.e("TAG","进入到主协程代码块finish")
}
Log.e("TAG","进入到主协程代码块履行完毕")

此刻是经过runBlocking的效果域上下文创立了一个协程,这个时分履行成果咱们看到是一向堵塞到协程履行完结之后。

2023-06-22 00:05:27.253 2501-2501/com.lay.nowinandroid E/TAG: 进入到主协程代码块
2023-06-22 00:05:27.256 2501-2501/com.lay.nowinandroid E/TAG: 进入到主协程代码块finish
2023-06-22 00:05:28.300 2501-2501/com.lay.nowinandroid E/TAG: 发动一个新的协程finish
2023-06-22 00:05:28.301 2501-2501/com.lay.nowinandroid E/TAG: 进入到主协程代码块履行完毕

所以一开始的比如中,咱们经过GlobalScope创立的一个协程并不在runBlocking的上下文效果域中创立的,所以就不会堵塞到协程履行完结,假如一定要比及其履行完结,能够经过join函数来完结等候。

runBlocking {
    Log.e("TAG","进入到主协程代码块")
    val job = GlobalScope.launch {
        delay(1000)
        Log.e("TAG","发动一个新的协程finish")
    }
    //手动堵塞
    job.join()
    Log.e("TAG","进入到主协程代码块finish")
}
Log.e("TAG","进入到主协程代码块履行完毕")

可是假如咱们经过手动坚持一切发动的协程引证,调用join办法很容易会出错,所以在面临结构化的并发问题时,咱们需求采用的便是运用runBlocking的协程效果域创立线程,运用其特性来完结并发。

所以假如在某个事务场景傍边,要求多个异步使命都履行完结之后,才干够持续往后履行,传统Java开发中,或许需求处理多个异步使命,然后再拿到统一的成果,需求敞开多个线程。而在Kotlin中则只需求经过runBlocking合作协程堵塞,就能够完结详细的效果。

var a = ""
var b = ""
var c = ""
runBlocking {
    Log.e("TAG", "进入到主协程代码块")
    launch {
        delay(2000)
        a = "这是异步使命A的成果"
    }
    launch {
        delay(1500)
        b = "这是异步使命B的成果"
    }
    launch {
        delay(300)
        c = "这是异步使命C的成果"
    }
    Log.e("TAG", "进入到主协程代码块finish")
}
Log.e("TAG", "成果:$a $b $c")
2023-06-22 00:16:54.810 4519-4519/com.lay.nowinandroid E/TAG: 进入到主协程代码块
2023-06-22 00:16:54.814 4519-4519/com.lay.nowinandroid E/TAG: 进入到主协程代码块finish
2023-06-22 00:16:56.861 4519-4519/com.lay.nowinandroid E/TAG: 成果:这是异步使命A的成果 这是异步使命B的成果 这是异步使命C的成果

假如是Java开发的同伴,能够把Java的完结办法贴在谈论区。

1.2.2 coroutineScope协程效果域

像在runBlocking效果域下的协程,其实是没有履行次序的,它只会在一切的协程履行完结之后,持续主线程的履行;假如在某个事务场景中需求某个协程先履行完,然后再履行其他的协程,那么能够运用coroutineScope来创立一个协程。

coroutineScope创立的协程效果域,会比及其一切的子协程都履行完结之后才会完毕,持续后面的协程履行。

var a = 0
var b = 0
runBlocking {
    coroutineScope {
        delay(1000)
        a = 20
        Log.e("TAG", "协程B完结")
    }
    launch {
        delay(300)
        b = a + 20
        Log.e("TAG", "协程A完结")
    }
    Log.e("TAG", "A-----B__--")
}
Log.e("TAG", "主线程持续履行 $a $b")
2023-06-22 00:33:41.816 7299-7299/com.lay.nowinandroid E/TAG: 协程B完结
2023-06-22 00:33:41.818 7299-7299/com.lay.nowinandroid E/TAG: A-----B__--
2023-06-22 00:33:42.161 7299-7299/com.lay.nowinandroid E/TAG: 协程A完结
2023-06-22 00:33:42.162 7299-7299/com.lay.nowinandroid E/TAG: 主线程持续履行 20 40

经过上面的比如咱们看到,正常状况下,调用delay会将协程挂起,然后开释底层线程去干其他的事,正常状况下会先打印出“A—–B__–”,可是并没有,而是彻底堵塞比及coroutineScope效果域内部子协程彻底履行完结之后才会履行后续的。

当然在coroutineScope内部,仍是会按照正常的挂起函数履行次序履行,例如:

var a = 0
var b = 0
runBlocking {
    GlobalScope.launch {
        delay(2000)
        Log.e("TAG","GlobalScope 协程 履行完结")
    }
    coroutineScope {
        launch {
            delay(200)
            Log.e("TAG","coroutineScope 协程1 履行完结")
        }
        Log.e("TAG","------1-------")
        delay(1000)
        a = 20
        Log.e("TAG", "协程B完结")
    }
    launch {
        delay(300)
        b = a + 20
        Log.e("TAG", "协程A完结")
    }
    Log.e("TAG", "A-----B__--")
}
Log.e("TAG", "主线程持续履行 $a $b")

感兴趣的同伴,能够把这段代码履行的成果发在谈论区,看下关于协程效果域的了解是否现已彻底掌握了。

回到coroutineScope这上面来,经过源码咱们发现coroutineScope是一个挂起函数,也便是当履行到coroutineScope代码块时,会将当时协程挂起也便是runBlocking这个主协程会被挂起,然后开释线程去做其他的作业,这个时分runBlocking内部其实是没法做事的,只能比及康复之后(coroutineScope内部协程履行完毕),才干持续履行后面的代码,可是又由于runBlocking的特性堵塞线程,所以runBlocking外部的主线程也无法履行,然后持续堵塞着。

public suspend fun <R> coroutineScope(block: suspend CoroutineScope.() -> R): R {
    contract {
        callsInPlace(block, InvocationKind.EXACTLY_ONCE)
    }
    return suspendCoroutineUninterceptedOrReturn { uCont ->
        val coroutine = ScopeCoroutine(uCont.context, uCont)
        coroutine.startUndispatchedOrReturn(coroutine, block)
    }
}

1.3 协程的撤销与超时

1.3.1 协程的撤销

当咱们运用一个协程时,或许并不会比及其彻底履行完毕自动退出,中间需求经过人为的干预撤销协程,例如两个协程在履行异步使命,只需有一个使命回来了成果,那么另一个协程就需求撤销。

fun test1() = runBlocking<Unit> {
    val job = launch {
        repeat(1000){
            delay(200)
            Log.e("TAG","now is $it")
        }
    }
    //3s后使命撤销
    delay(2000)
    Log.e("TAG","2s后,撤销协程")
    job.cancel()
}

正常状况下,test1办法会比及1000次的打印使命完结之后退出,可是咱们手动增加一个退出使命,便是在2s后撤销协程,调用cancel办法。

2023-06-22 11:39:27.396 10451-10451/com.lay.nowinandroid E/TAG: now is 0
2023-06-22 11:39:27.632 10451-10451/com.lay.nowinandroid E/TAG: now is 1
2023-06-22 11:39:27.853 10451-10451/com.lay.nowinandroid E/TAG: now is 2
2023-06-22 11:39:28.082 10451-10451/com.lay.nowinandroid E/TAG: now is 3
2023-06-22 11:39:28.323 10451-10451/com.lay.nowinandroid E/TAG: now is 4
2023-06-22 11:39:28.564 10451-10451/com.lay.nowinandroid E/TAG: now is 5
2023-06-22 11:39:28.806 10451-10451/com.lay.nowinandroid E/TAG: now is 6
2023-06-22 11:39:29.048 10451-10451/com.lay.nowinandroid E/TAG: now is 7
2023-06-22 11:39:29.192 10451-10451/com.lay.nowinandroid E/TAG: 2s后,撤销协程

经过日志咱们发现,的确是撤销了协程。可是撤销其实会有副效果的,经过源码咱们能够看到:

public fun cancel(cause: CancellationException? = null)

当子协程撤销之后,会抛出一个CancellationException反常,此刻父协程会接纳到这个反常并决定是否需求处理反常,所以咱们一般都会对反常比较敏感,分明是一个正常的撤销操作,偏偏是经过抛反常的行为来进行协程撤销,那么协程一定会被撤销吗?

答案是不一定,在某些场景下,例如CPU的密集型使命时仅仅调用cancel是不会被立即撤销的,看下面的比如

fun test2() = runBlocking {
    val job = launch {
        for (i in 0..100) {
            Log.e("TAG", "now is $i")
        }
    }
    delay(5)
    Log.e("TAG", "预备撤销......")
    job.cancelAndJoin()
    Log.e("TAG", "完毕使命")
}

在子协程中一向在履行循环使命,此刻一向在占用CPU,而父协程内仅仅挂起了5ms,只为确保子协程内部使命敞开,这时咱们发现只有当循环使命完结之后,才被撤销,但这时撤销其完结已没有意义了。

所以划要点,协程的撤销是协作的,什么是协作呢?便是在协程中,一切的挂起函数都是能够被撤销的,同伴们看好,是只有“挂起函数”才干够被撤销!,它会在挂起时查看协程是否被撤销,假如撤销那么会抛出CancellationException反常,那么当协程在履行CPU密集型使命时,是没有查看撤销的(由于没有挂起的这个契机,除非加上delay等挂起函数),所以协程便不能被撤销

fun test1() = runBlocking<Unit> {
    val job = launch {
        repeat(1000) {
            try {
                delay(200)
            }catch (e:Exception){
                Log.e("TAG","exp-->$e")
            }
            Log.e("TAG", "now is $it")
        }
    }
    //3s后使命撤销
    delay(2000)
    Log.e("TAG", "2s后,撤销协程")
    job.cancel()
}

已然只有挂起函数,才会自动查看协程是否被撤销,那么咱们试一下当履行delay挂起函数时,catch一下看是否能够捕获到反常;

2023-06-22 13:05:25.864 23519-23519/com.lay.nowinandroid E/TAG: now is 6
2023-06-22 13:05:26.084 23519-23519/com.lay.nowinandroid E/TAG: now is 7
2023-06-22 13:05:26.245 23519-23519/com.lay.nowinandroid E/TAG: 2s后,撤销协程
2023-06-22 13:05:26.249 23519-23519/com.lay.nowinandroid E/TAG: exp-->kotlinx.coroutines.JobCancellationException: StandaloneCoroutine was cancelled; job=StandaloneCoroutine{Cancelling}@72b055a
2023-06-22 13:05:26.249 23519-23519/com.lay.nowinandroid E/TAG: now is 8
2023-06-22 13:05:26.249 23519-23519/com.lay.nowinandroid E/TAG: exp-->kotlinx.coroutines.JobCancellationException: StandaloneCoroutine was cancelled; job=StandaloneCoroutine{Cancelling}@72b055a
2023-06-22 13:05:26.249 23519-23519/com.lay.nowinandroid E/TAG: now is 9

果然,当咱们履行cancel函数之后,再次履行delay挂起函数时,捕获到了JobCancellationException反常, 由于咱们在子协程中捕获了反常,父协程无法接纳反常,所以撤销操作就没有生效,这和咱们之前的定论是共同的。

所以当履行核算作业,或许CPU密集型使命时,假如没有挂起函数时假如想要撤销协程,其实是有2种方案可用。

  • 定时运用挂起函数查看协程是否被撤销,常见的为yield函数
fun test2() = runBlocking {
    val job = launch{
        for (i in 0..100) {
            yield()
            Log.e("TAG", "now is $i $isActive")
        }
    }
    delay(5)
    Log.e("TAG", "预备撤销......")
    job.cancel()
    Log.e("TAG", "完毕使命")
}

由于yield函数归于挂起函数,当进行CPU密集型使命时,每次都会履行一次yield,查看当时协程的状况是否被撤销,假如撤销那么将不再履行使命,其实和其他一般的挂起函数相同,yield同样也是为了开释底层线程干其他的作业。

  • 运用isActive或许ensureActive判别当时协程是否活泼

当协程被撤销之后,isActive就会变为false,此刻进入到Cancelling撤销中的状况,直到协程撤销进入到Cancelled状况。

fun test2() = runBlocking {
    val job = launch(Dispatchers.Default) {
        for (i in 0..1000) {
            if (isActive) {
                Log.e("TAG", "now is $i")
            } else {
                break
            }
        }
    }
    delay(5)
    Log.e("TAG", "预备撤销......")
    job.cancelAndJoin()
    Log.e("TAG", "完毕使命")
}

这儿我把量级加到了1000,由于核算速度太快或许无法比及协程撤销,并且声明晰协程效果域上下文为Dispatchers.Default,这儿为啥需求生命为此,后续再介绍。

2023-06-22 13:53:22.162 31488-31519/com.lay.nowinandroid E/TAG: now is 844
2023-06-22 13:53:22.163 31488-31519/com.lay.nowinandroid E/TAG: now is 845
2023-06-22 13:53:22.163 31488-31519/com.lay.nowinandroid E/TAG: now is 846
2023-06-22 13:53:22.164 31488-31488/com.lay.nowinandroid E/TAG: 完毕使命

最终在履行到846次循环时,协程被撤销了。

fun test2() = runBlocking {
    val job = launch(Dispatchers.Default) {
        for (i in 0..1000) {
            ensureActive()
            Log.e("TAG", "now is $i")
        }
    }
    delay(5)
    Log.e("TAG", "预备撤销......")
    job.cancelAndJoin()
    Log.e("TAG", "完毕使命")
}

除此之外,还能够经过调用ensureActive来判别当时协程的状况,假如不是活泼状况,那么就会直接抛CancellationException反常,这就有点类似于挂起函数的效果了。

public fun Job.ensureActive(): Unit {
    if (!isActive) throw getCancellationException()
}

1.3.2 协程撤销时的资源开释

当咱们撤销协程时,一般都会做一些资源开释作业,此刻能够放在finally中进行,例如:

fun test1() = runBlocking<Unit> {
    val job = launch {
        try {
            repeat(1000) {
                delay(200)
                Log.e("TAG", "now is $it")
            }
        } finally {
            Log.e("TAG", "do release")
        }
    }
    //3s后使命撤销
    delay(2000)
    Log.e("TAG", "2s后,撤销协程")
    job.cancel()
}
2023-06-22 14:11:36.137 2857-2857/com.lay.nowinandroid E/TAG: now is 7
2023-06-22 14:11:36.368 2857-2857/com.lay.nowinandroid E/TAG: now is 8
2023-06-22 14:11:36.369 2857-2857/com.lay.nowinandroid E/TAG: 2s后,撤销协程
2023-06-22 14:11:36.372 2857-2857/com.lay.nowinandroid E/TAG: do release

此刻在finally中完结开释作业时,协程现已是Canceled的状况,此刻再次调用挂起函数,就会抛CancellationException,例如下面的代码:

fun test1() = runBlocking<Unit> {
    val job = launch {
        try {
            repeat(1000) {
                delay(200)
                Log.e("TAG", "now is $it")
            }
        } finally {
            Log.e("TAG", "do release")
            delay(200)
            Log.e("TAG","suspend again")
        }
    }
    //3s后使命撤销
    delay(2000)
    Log.e("TAG", "2s后,撤销协程")
    job.cancel()
}

当运转后,“suspend again”不会被履行打印,由于现已发生了反常,这点要特别注意,假如需求在finally中运用挂起函数,也便是要挂起一个被撤销的协程,那么能够运用withContext合作NonCancellable上下文。

fun test1() = runBlocking<Unit> {
    val job = launch {
        try {
            repeat(1000) {
                delay(200)
                Log.e("TAG", "now is $it")
            }
        } finally {
            Log.e("TAG", "do release")
            withContext(NonCancellable) {
                delay(200)
                Log.e("TAG", "suspend again")
            }
        }
    }
    //3s后使命撤销
    delay(2000)
    Log.e("TAG", "2s后,撤销协程")
    job.cancel()
}

1.3.2 协程超时

其实超时在事务场景中是很常见的,当进行网络请求时受到网络动摇,导致成果回来超时,假如咱们运用OkHttp等网络框架时,也会设置一次请求的超时时间,而在协程傍边,咱们同样能够设置withTimeout超时函数来撤销某个协程。

fun testTimeout() = runBlocking {
    try {
        //设置2s超时时间
        withTimeout(2000) {
            delay(2500)
            Log.e("TAG", "拿到了服务端回来的成果")
        }
    }catch (e:Exception){
        Log.e("TAG","exp --> $e")
    }
    Log.e("TAG","finish")
}

这儿咱们设置了2s的超时时间,咱们模拟的拿到服务端成果为2.5s,那么此刻现已超时,咱们打印成果发现抛出了TimeoutCancellationException反常。

2023-06-22 14:32:05.284 6570-6570/com.lay.nowinandroid E/TAG: exp --> kotlinx.coroutines.TimeoutCancellationException: Timed out waiting for 2000 ms

经过源码咱们能够看到,TimeoutCancellationException是CancellationException的子类,也便是说当抛出超时反常之后,当时协程也一同被撤销了。

public class TimeoutCancellationException internal constructor(
    message: String,
    @JvmField @Transient internal val coroutine: Job?
) : CancellationException(message), CopyableThrowable<TimeoutCancellationException> {
    /**
     * Creates a timeout exception with the given message.
     * This constructor is needed for exception stack-traces recovery.
     */
    internal constructor(message: String) : this(message, null)
    // message is never null in fact
    override fun createCopy(): TimeoutCancellationException =
        TimeoutCancellationException(message ?: "", coroutine).also { it.initCause(this) }
}

除此之外,还能够经过withTimeoutOrNull函数,默认回来一个超时之后的值便是null。同前面关于协程撤销时资源开释问题,当协程超时时,也能够放在finally中进行资源开释。

首要咱们能够先看官方的一个比如:

fun testFinally() = runBlocking {
    Log.e("TAG","start ---- ")
    repeat(10000) {
        launch {
            val resource = withTimeout(60) {
                delay(50)
                Resource()
            }
            resource.close()
        }
    }
}
Log.e("TAG","count $count")
var count = 0
class Resource {
    init {
        count++
    }
    fun close() {
        count--
    }
}

每当创立一个Resource对象,count都会+1,每次调用close收回资源,count都会-1,如此一来,只需创立Resource时没有超时,那么一定为0;一旦在某个时间创立Resource超时,那么Resource就不为0了,由于协程被撤销,导致close函数没有被及时调用。

所以为了避免这种状况发生,正确的开释资源的姿态应该是放在finally傍边,这时分假如发生超时,那么一定会调用close函数。

fun testFinally() = runBlocking {
    Log.e("TAG", "start ---- ")
    repeat(10000) {
        launch {
            var resource: Resource? = null
            try {
                withTimeout(60) {
                    delay(50)
                    resource = Resource()
                }
            } finally {
                resource?.close()
            }
        }
    }
}
Log.e("TAG", "count $count")

由于我现在用的是mac,装备还不错,导致一向测验复现第一种写法没有成功,一向是0,有精力的同伴能够测验复现一下。

关于协程,或许许多同伴们在开发傍边都用到过,但关于某些常识点,例如不同协程构建器构建的协程内部履行规则,关于有些同伴或许便是常识盲区,然而经过翻阅大部分的材料功率并不高,我这儿也是为大家整理出来方便同伴们学习掌握。