Kotlin 协程(二) -协程取消与超时

协程一:Kotlin 协程 (一)

在长时间运行的程序中,如果协程的执行结果不需要了,那么协程是可以取消的,使用Job,cancel()函数执行

示例(1):

fun main() = runBlocking {

    val job = launch {

        // repeat 函数是一个内联函数,repeat(500) 表示重复执行500次

        repeat(500) { i ->

            println("Hello $i")

            delay(500)

        }

    }

    delay(1800)

    println("World")

    // 取消协程该次操作

    job.cancel()

    // 可以在一个作业中等待另一个作业结束后再进行其他操作

    job.join() // 等待作业执行结束

    println("Main函数退出了...")

}

示例(1)执行结果:

Hello 0

Hello 1

Hello 2

Hello 3

World

Main函数退出了...

通过示例(1)可以看出,如果协程没有取消,println("Hello $i")函数应该执行500次,而协程在延时1800毫秒之后取消了,所以println("Hello $i")函数只执行了4次,协程取消之后,函数就不再执行了

Job的cancel()和join()函数可以合并成一个挂起函数cancelAndJoin(),该挂起函数的作用是取消当前作业,并等待作业执行结束.

示例(2):

fun main() = runBlocking {

    val job = launch {

        // repeat 函数是一个内联函数,repeat(500) 表示重复执行500次

        repeat(500) { i ->

            println("Hello $i")

            delay(500)

        }

    }

    delay(1800)

    println("World")

    // 取消当前作业,并等待作业执行结束

    job.cancelAndJoin()

    println("Main函数退出了...")

}

示例(2)执行结果:

Hello 0

Hello 1

Hello 2

Hello 3

World

Main函数退出了...

在kotlinx.coroutines包下的所有挂起函数都是可被取消的.它们检查协程的取消,并在取消时抛出 CancellationException. 不过,如果协程正在执行计算任务,并且没有检查取消的话,那么它是不能被取消的

示例(3):

fun main() = runBlocking {

    // 开始时间

    val startTime = System.currentTimeMillis()

    val job = launch(Dispatchers.Default) {

        // 下一次打印时间

        var nextPrintTime = startTime

        var i = 0

        while (i < 10) { // 循环计算

            if (System.currentTimeMillis() >= nextPrintTime) {

                println("job sleeping ${i++} ")

                nextPrintTime += 500

            }

        }

    }

    delay(1800)

    println("Hello World")

    job.cancelAndJoin()

    println("Main函数退出了...")

}

示例(3)执行结果:

job sleeping 0

job sleeping 1

job sleeping 2

job sleeping 3

Hello World

job sleeping 4

job sleeping 5

job sleeping 6

job sleeping 7

job sleeping 8

job sleeping 9

Main函数退出了...

通过示例(3)可以看到,在调用cancel()之后,还继续打印 job sleeping,直到运行结束.说明协程正在执行计算任务,并且没有检查取消的话,那么它是不能被取消的

如果想取消正在计算的job,可以定期调用挂起函数来检查取消,如yield()函数.或者是调用显式的检查取消状态,如示例(4)

示例4:

fun main() = runBlocking {

    // 开始时间

    val startTime = System.currentTimeMillis()

    val job = launch(Dispatchers.Default) {

        // 下一次打印时间

        var nextPrintTime = startTime

        var i = 0

       while (isActive) { // 可以被取消的计算循环

            if (System.currentTimeMillis() >= nextPrintTime) {

                println("job sleeping ${i++} ")

                nextPrintTime += 500

            }

        }

    }

    delay(1800)

    println("Hello World")

    job.cancelAndJoin()

    println("Main函数退出了...")

}

示例(4)执行结果:

job sleeping 0

job sleeping 1

job sleeping 2

job sleeping 3

Hello World

Main函数退出了...

通过示例(4)可以看到,现在循环被取消了.isActive 是一个可以被使用在 CoroutineScope 中的扩展属性

job还可以在 finally 中释放资源

示例(5):

fun main() = runBlocking {

    val job = launch {

        // repeat 函数是一个内联函数,repeat(500) 表示重复执行500次

        try {

            repeat(500) { i ->

                println("Hello $i")

                delay(500)

            }

        } finally {

            println("执行了finally块...")

        }

    }

    delay(1800)

    println("World")

    // 取消当前作业,并等待作业执行结束

    job.cancelAndJoin()

    println("Main函数退出了...")

}

示例(5)执行结果:

Hello 0

Hello 1

Hello 2

Hello 3

World

执行了finally块...

Main函数退出了...

如果在使用finally取消job,且在finally中执行挂起函数,由于job作业被取消了,所以finally里面的挂起函数将不会被执行

示例(6):

fun main() = runBlocking {

    val job = launch {

        // repeat 函数是一个内联函数,repeat(500) 表示重复执行500次

        try {

            repeat(500) { i ->

                println("Hello $i")

                delay(500)

            }

        } finally {

            println("执行了finally块...")

            delay(500)

            println("在finally 中执行delay之后...")

        }

    }

    delay(1800)

    println("World")

    // 取消当前作业,并等待作业执行结束

    job.cancelAndJoin()

    println("Main函数退出了...")

}

示例(6)执行结果:

Hello 0

Hello 1

Hello 2

Hello 3

World

执行了finally块...

Main函数退出了...

从示例(6)的执行结果可以看出,的job作业被取消之后的挂起函数没有得到执行,如果需要挂起一个被取消的协程,可以将相应的代码包装在 withContext(NonCancellable) {……} 中,并使用 withContext 函数以及 NonCancellable 上下文

示例(7):

fun main() = runBlocking {

    val job = launch {

        // repeat 函数是一个内联函数,repeat(500) 表示重复执行500次

        try {

            repeat(500) { i ->

                println("Hello $i")

                delay(500)

            }

        } finally {

            withContext(NonCancellable){

                println("执行了finally块...")

                delay(500)

                println("在finally 中执行delay之后...")

            }

        }

    }

    delay(1800)

    println("World")

    // 取消当前作业,并等待作业执行结束

    job.cancelAndJoin()

    println("Main函数退出了...")

}

示例(7)执行结果:

Hello 0

Hello 1

Hello 2

Hello 3

World

执行了finally块...

在finally 中执行delay之后...

Main函数退出了...

在使用协程的过程中,绝大多数取消一个协程的原因是因为它有可能超时.当你手动追踪一个相关 Job 的引用并启动了一个单独的协程在延迟后取消追踪,这里已经准备好使用 withTimeout 函数来做这件事

示例(8):

fun main() = runBlocking {

    withTimeout(1800){

        repeat(1000) {

            i ->

            println("Hello $i")

            delay(400)

        }

    }

}

示例(8)执行结果:

hello 0

hello 1

hello 2

hello 3

hello 4

Exception in thread "main" kotlinx.coroutines.TimeoutCancellationException: Timed out waiting for 1900 ms

    at kotlinx.coroutines.TimeoutKt.TimeoutCancellationException(Timeout.kt:116)

    at kotlinx.coroutines.TimeoutCoroutine.run(Timeout.kt:86)

    at kotlinx.coroutines.EventLoopImplBase$DelayedRunnableTask.run(EventLoop.common.kt:492)

    at kotlinx.coroutines.EventLoopImplBase.processNextEvent(EventLoop.common.kt:271)

    at kotlinx.coroutines.DefaultExecutor.run(DefaultExecutor.kt:68)

    at java.lang.Thread.run(Thread.java:748)

从示例(8)可以看到,由withTimeout函数调用所抛出的TimeoutCancellationException异常是CancellationException的子类,当该异常抛出时,我们并未在控制台上看到整个异常堆栈信息,这是因为我们取消的协程当中,CancellationException被认为是一种协程完成的正常原因而已

如果你需要做一些各类使用超时的特别的额外操作,可以使用类似 withTimeout 的 withTimeoutOrNull 函数,并把这些会超时的代码包装在 try {...} catch (e: TimeoutCancellationException) {...} 代码块中,而 withTimeoutOrNull 通过返回 null 来进行超时操作,从而替代抛出一个异常:

示例(9):

fun main() = runBlocking {

    val result = withTimeoutOrNull(1800){

        repeat(1000) {

            i ->

            println("Hello $i")

            delay(300)

        }

        "Hello World"

    }

    println("Result is $result")

}

示例(9)执行结果:

Hello 0

Hello 1

Hello 2

Hello 3

Hello 4

Hello 5

Result is null

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 217,542评论 6 504
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,822评论 3 394
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 163,912评论 0 354
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,449评论 1 293
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,500评论 6 392
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,370评论 1 302
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,193评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,074评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,505评论 1 314
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,722评论 3 335
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,841评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,569评论 5 345
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,168评论 3 328
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,783评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,918评论 1 269
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,962评论 2 370
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,781评论 2 354

推荐阅读更多精彩内容