在之前的文章中,已经讲了如何启动协程、协程的作用域是如何组织和工作的以及各种协程构造器(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依然在执行线程的代码,非阻塞的暂停只是通过用户态的程序逻辑让代码块不执行而已。
用图来表示线程阻塞的情况应该是这样:
而在协程中,非阻塞的情况应该是这样:
可以看到,线程的阻塞,那这个线程就真的不去做事情了,必须等到被唤醒了,才会继续执行,在被唤醒之前,这个线程资源可以说就被浪费了,如果我有新的任务,就必须在启动一个新的线程来执行。
但是协程上的挂起,它会去寻找有没有需要执行的代码块,如果有,就拿来跑,这样就能更高效的利用线程资源。如果挂起后,也没有发现任何可以执行的代码块,同样的也会进入阻塞状态,这一点和线程是一样的。
在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函数来将逻辑切换到其他的线程上去。
之前的表格,就可以得到进一步的扩展了
相关阅读
如果你喜欢这篇文章,欢迎点赞评论打赏
更多干货内容,欢迎关注我的公众号:好奇码农君