在之前的文章中,已经讲了如何启动协程、协程的作用域是如何组织和工作的以及各种协程构造器(builder)的特性。

本篇将讲解对协程的各种操作,包括挂起、取消、超时、切换上下文等。

挂起

fun main()  {
    runBlocking(Dispatchers.Default) {
        for (i in 0 .. 10) {
            println("aaaaa ${Thread.currentThread().name}")
            delay(1000) // 这是一个挂起函数
            println("bbbbb ${Thread.currentThread().name}")
        }
    }
}

delay就是一个挂起函数,挂起的意思是:非阻塞的暂停,与之对应的就是阻塞(的暂停)。比如线程的方法Thread.sleep就是一个阻塞的方法。关于阻塞还是非阻塞,可以简单的理解为:

  • 阻塞就是cpu不执行后面的代码,需要某种通知告诉线程继续执行。
  • 非阻塞就是cpu依然在执行线程的代码,非阻塞的暂停只是通过用户态的程序逻辑让代码块不执行而已。

用图来表示线程阻塞的情况应该是这样:

clipboard.png

而在协程中,非阻塞的情况应该是这样:

clipboard.png

可以看到,线程的阻塞,那这个线程就真的不去做事情了,必须等到被唤醒了,才会继续执行,在被唤醒之前,这个线程资源可以说就被浪费了,如果我有新的任务,就必须在启动一个新的线程来执行。

但是协程上的挂起,它会去寻找有没有需要执行的代码块,如果有,就拿来跑,这样就能更高效的利用线程资源。如果挂起后,也没有发现任何可以执行的代码块,同样的也会进入阻塞状态,这一点和线程是一样的。

在kotlin中,挂起函数只能在协程环境中使用。

等待与取消

等待一个协程执行完毕,和线程的API一致,使用join方法就可以了。

val job = launch {
    // ....
}

job.join()

如果需要返回值,也可以使用async来启动协程,使用await方法来等待完成,并取得返回值数据。

val job = async {
    // ....
}

job.await()

await和join都是挂起函数。

协程应该被实现为可以被取消的,调用Job的cancel方法可以取消。但是,如果我们写个while(true)的死循环怎么取消呢?

显然是取消不了的。

为了能让我们的协程逻辑能被取消,就需要使用到协程的一个属性isActive。

假设我们有一个协程是下载一个文件,我们想让它能被取消。它可能是这样:

val dlJob = launch {
    var isFinished = false
    while (!isFinished) {
        // download ...
        
        if (dlSize == totalSize) {
            isFinished = true
        }
    }
}

这样的话,这个协程是无法被取消的,它无法被外侧所操控,我们可以使用isActive来改写一下。

val dlJob = launch {
    var isFinished = false
    while (!isFinished && isActive) { // 注意这里
        // download ...

        if (dlSize == totalSize) {
            isFinished = true
        }
    }
}

只需要这样,就可以实现取消逻辑了。

问题也就随之而来,像打开网络连接,读写文件,总是需要去执行一些close的逻辑才是符合规范的,如果协程被取消,就直接退出了,要如何才能回收打开的资源呢?

如何回收资源

可以通过try{...}finally{...}进行回收资源,就像这样:

val dlJob = launch {
    try {
        var isFinished = false
        while (!isFinished && isActive) { // 注意这里
            // download ...

            if (dlSize == totalSize) {
                isFinished = true
            }
        }    
    } finally {
        // close something
    }
}

当job被取消后,finally方法里面依然会在最后被执行,可以在这里进行一些回收的操作。

超时

如果我们期望一个协程最多只能执行多少时间,超过这个时间就要被取消的时候,就可以使用超时逻辑,可以使用withTimeout函数来实现。

runBlocking(Dispatchers.Default) {

    try {
        // 只允许协程执行最多500毫秒
        val job = withTimeout(500) { 
            try {
                println("working 1")
                delay(1000)
                println("working 2")
            } finally {
                println("finally, I will do something")
            }
        }

        println("job $job") // 无法被执行到
    } catch (e: Throwable) {
        println("out coroutine $e")
    }

}

如果超时了,则会抛异常,并且,这个函数与runBlocking是一样的,都会阻塞当前线程。上面的代码中,协程外的print不会被执行到。

如果不想抛异常,可以使用另一个超时函数withTimeoutOrNull。

runBlocking(Dispatchers.Default) {

    try {
        // 只允许协程执行最多500毫秒
        val job = withTimeoutOrNull(500) {
            try {
                println("working 1")
                delay(1000)
                println("working 2")
            } finally {
                println("finally, I will do something")
            }
        }
    
        println("job $job") // 可以被执行到
    } catch (e: Throwable) {
        println("out coroutine $e")
    }

}

最终运行的结果是:

working 1
finally, I will do something
job null

切换上下文

如果我们期望协程的代码在不同的线程中来回跳转,可以使用withContext来实现。(emmmmm,这是什么场景的需求呢?)

newSingleThreadContext("Ctx1").use { ctx1 ->
    newSingleThreadContext("Ctx2").use { ctx2 ->
        runBlocking(ctx1) {
            log("Started in ctx1")
            withContext(ctx2) {
                log("Working in ctx2")
            }
            log("Back to ctx1")
        }
    }
}

这里直接照搬文档中的示例代码,最后输出的结果为:

[Ctx1 @coroutine#1] Started in ctx1
[Ctx2 @coroutine#1] Working in ctx2
[Ctx1 @coroutine#1] Back to ctx1

总结

以上就是操控协程的各种方法了。

挂起函数是协程中定义的概念,只能在协程中使用,挂起的含义是非阻塞的暂停,调度器会寻找需要运行的协程放到线程中去执行,如果找不到任何需要执行的协程,才会将线程阻塞。

协程是可以被取消的,任何系统提供的挂起函数内部都有取消的逻辑,如果自己的协程想要可以被取消,就必须通过isActive变量来编写逻辑。

取消后的协程总是会执行finally代码块,可以在这里进行一些资源回收的操作。

如果希望控制协程的工作时长,可以使用withTimeout来限制协程。

通过withContext函数来将逻辑切换到其他的线程上去。

之前的表格,就可以得到进一步的扩展了

clipboard.png

相关阅读

如果你喜欢这篇文章,欢迎点赞评论打赏
更多干货内容,欢迎关注我的公众号:好奇码农君


krosshj
152 声望16 粉丝

Developer, Gamer, Artist