Kotlin协程教程3操控协程

63次阅读

共计 3014 个字符,预计需要花费 8 分钟才能阅读完成。

在之前的文章中,已经讲了如何启动协程、协程的作用域是如何组织和工作的以及各种协程构造器(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 函数来将逻辑切换到其他的线程上去。

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

相关阅读

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

正文完
 0