本文基于协程官方文档讲解,具体可查看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")
}
本应该在"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")
}
取消后,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")
}
小结一下:协程取消可能不如我们想的那样,及时取消,要善于用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")
}
注意一下: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")
}
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()
}
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个任务给线程池执行。
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()
}
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")
}
注意上面👆这个例子用了一个单线程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")
}