关于长时间做过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,有精力的同伴能够测验复现一下。
关于协程,或许许多同伴们在开发傍边都用到过,但关于某些常识点,例如不同协程构建器构建的协程内部履行规则,关于有些同伴或许便是常识盲区,然而经过翻阅大部分的材料功率并不高,我这儿也是为大家整理出来方便同伴们学习掌握。