协程系列(三)Cancellation and timeouts

856 阅读3分钟

本文基于协程官方文档讲解,具体可查看here

一、取消异常

一般情形下,我们使用job.cancel()去取消协程,但是,你的协程程序真的及时取消了吗?
先看个例子

fun printMsg(msg:String){
   println("${Thread.currentThread().name}  "+msg)
}
fun main() = runBlocking {
    val startTime = System.currentTimeMillis()
    val job = launch(Dispatchers.Default) {
        var nextPrintTime = startTime
        var i = 0;
        while (i < 5) {
            if (System.currentTimeMillis() >= nextPrintTime) {
                nextPrintTime += 500L;
                printMsg("job-->${i++}")
            }
        }
    }
    delay(1000)
    printMsg("main i am waiting")
    job.cancelAndJoin()
    printMsg("main i can quit")
 }

image.png 本应该在"main i am waiting"打印后,就立马取消的,但是这里还打印了3和4,完全不应该的。
问题在哪里,job.cancelAndJob()其实就是job.cancel()+job.join()`,取消肯定会抛出一个CancellationException,挂起函数肯定会响应这个异常,导致程序不再执行。同样取消job后,job的状态isActive为false。但这里的代码,没有好好利用这些细节,导致程序继续运行着, 不要小看这些,一旦没有及时终止程序,内存泄露就可能出现了。

1.1、通过判断job是否isActive解决

fun main() = runBlocking {
    val startTime = System.currentTimeMillis()
    val job = launch(Dispatchers.Default) {
        var nextPrintTime = startTime
        var i = 0
        while (isActive) {  //很关键的代码
            if (System.currentTimeMillis() >= nextPrintTime) {
                nextPrintTime += 500L;
                printMsg("job-->${i++}")
            }
        }
    }
    delay(1000)
    printMsg("main i am waiting")
    job.cancelAndJoin()
    printMsg("main i can quit")
}

image.png 取消后,isActive为false,可以及时终止程序运行。

1.2、通过添加真正的挂起函数解决

比如yield() 和 delay(time) time>0. (time=0的话,delay不会挂起,所以没效果)
yield() 主要作用将当前协程调度器的线程(或线程池)交给同一调度器上的其他协程运行

fun main() = runBlocking {
    val startTime = System.currentTimeMillis()
    val job = launch(Dispatchers.Default) {
        var nextPrintTime = startTime
        var i = 0
        while (i<5) {
            yield()  //挂起函数 换成delay(1) 也是可以正常取消的
            if (System.currentTimeMillis() >= nextPrintTime) {
                nextPrintTime += 500L
                printMsg("job-->${i++}")
            }
        }
    }
    delay(1000)
    printMsg("main i am waiting")
    job.cancelAndJoin()
    printMsg("main i can quit")
}

image.png

小结一下:协程取消可能不如我们想的那样,及时取消,要善于用isActive或者挂起函数解决。

二、取消时协程体执行try finally

2.1、协程取消会抛出CancellationException,可以被try catch捕捉, finally处可以执行finalization actions。

fun main() = runBlocking {
    val job = launch(Dispatchers.Default) {
        try {
            repeat(1000) {
                printMsg("job i am sleeping ")
                delay(500L)
            }
        } catch (e: Exception) {
            printMsg(e.message + ".................")
        } finally {
            printMsg("finally .....")
        }
    }
    delay(1000)
    printMsg("main i am waiting")
    job.cancelAndJoin()
    printMsg("main i can quit")
}

image.png 注意一下:try catch finally 是在协程体里面,不是协程外面,当然啦,不要catch也可以的。

如果我们想在finally处,延迟一会,采用下面👇的代码,你会发现他是没用的。

} finally {
    delay(1000)
    printMsg("finally .....")
}

2.2、如finally处要delay,可用withContext(NonCancellable)

fun main() = runBlocking {
    val job = launch(Dispatchers.Default) {
        try {
            repeat(1000) {
                printMsg("job i am sleeping ")
                delay(500L)
            }
        }finally { //关键点
            withContext(NonCancellable){
                printMsg("job I am running finally")
                delay(3000L)
                printMsg("delay 3s")
            }
        }
    }
    delay(1000)
    printMsg("main i am waiting")
    job.cancelAndJoin()
    printMsg("main i can quit")
}

image.png withContext(NonCancellable)比较厉害,逃出了当前要被取消的命运,等待其执行完。

三、超时timeout

3.1、 withTimeout超时会抛超时取消异常

fun main() = runBlocking {
    val job = launch(Dispatchers.Default) {
        try {
            withTimeout(1300) {
                repeat(1000) {
                    printMsg("i am sleep ${it}")
                    delay(500)
                }
            }
        }catch (e:Exception){
            printMsg("$e")
        }
    }
    job.join()
}

image.png delay是挂起函数,delay时间累计。肯定会超时抛异常。

但是代码写成下面👇这样会超时吗?No!!!

fun main() = runBlocking {
    val job = launch(Dispatchers.Default) {
        try {
            withTimeout(1300) {
                repeat(1000) {
                   launch {  //开启1000个协程
                       printMsg("i am sleep ${it}")
                       delay(500)
                   }
                }
            }
        }catch (e:Exception){
            printMsg("$e")
        }
    }
    job.join()
}

开启了1000个协程,那么就是1000个任务给线程池执行。 image.png

3.2、withTimeoutOrNull超时返回结果当null处理

fun main() = runBlocking {
    val job = launch(Dispatchers.Default) {
        val result= withTimeoutOrNull(1300) {
            repeat(1000) {
                printMsg("i am sleep ${it}")
                delay(500)
            }
            "Done"
        }
        printMsg("result  ${result}")
    }
    job.join()
}

image.png

3.3、withTimeout搭配try finally释放资源防止资源泄露

var acquired = 0
class Resource {
    init { acquired++ }
    fun close() { acquired-- }
}
fun main() { //这里在单线程执行
    runBlocking(newSingleThreadContext("xxx")) {
        repeat(100_000) {
            launch {
                var resource: Resource? = null
                try {
                    withTimeout(60) {
                        delay(50)
                        resource = Resource()
                        printMsg("获取资源 $acquired")
                    }
                } finally {
                    resource?.close()
                    printMsg("释放资源 $acquired")
                }
            }
        }
    }
    printMsg("$acquired")
}

image.png 注意上面👆这个例子用了一个单线程newSingleThreadContext("xxx")来执行任务。

如果你用下面👇这个:

runBlocking(Dispatchers.Default) {xxx}

就存在线程安全问题,这个acquired的值可能不为0.如果要解决的话,可以用原子类。

var acquired = AtomicInteger(0)
class Resource {
    init { acquired.incrementAndGet() }
    fun close() { acquired.decrementAndGet() }
}
fun main() {
    runBlocking(Dispatchers.Default) { //线程池执行任务
        repeat(100_000) {
            launch {
                var resource: Resource? = null
                try {
                    withTimeout(60) {
                        delay(50)
                        resource = Resource()
                        printMsg("获取资源 $acquired")
                    }
                } finally {
                    resource?.close()
                    printMsg("释放资源 $acquired")
                }
            }
        }
    }
    printMsg("$acquired")
}

image.png