协程概念
协程是Coroutine的中文简称,co表示协同、协作,routine表示程序。协程可以理解为多个互相协作的程序。协程是轻量级的线程,它的轻量体现在启动和切换,协程的启动不需要申请额外的堆栈空间;协程的切换发生在用户态,而非内核态,避免了复杂的系统调用。
特点
- 1)更加轻量级,占用资源更少。
- 2)避免“回调地狱”,增加代码可读性。
- 3)协程的挂起不阻塞线程。
协程的描述:
- 协程就像非常轻量级的线程。线程是由系统调度的,线程切换或线程阻塞的开销比较大。
- 而协程依赖于线程,但是协程挂起时不需要阻塞线程,几乎是无代价的,协程是由开发者控制的。所以协程也像用户态的线程,非常轻量级,一个线程中可以创建任意个协程。
- 总而言之:协程可以简化异步编程,可以顺序地表达程序,协程也提供了一种避免阻塞线程并用更廉价、更可控的操作替代线程阻塞的方法—协程挂起。
Kotlin 协程的基本使用
讲概念之前,先讲用法。
场景: 开启工作线程执行一段耗时任务,然后在主线程对结果进行处理。
常见的处理方式:
- 自己定义回调,进行处理
- 使用 线程/线程池, Callable、线程 Thread(FeatureTask(Callable)).start、线程池 submit(Callable)
- Android: Handler、 AsyncTask、 Rxjava
使用协程:
coroutineScope.launch(Dispatchers.Main) { // 在主线程启动一个协程
val result = withContext(Dispatchers.Default) { // 切换到子线程执行
doSomething() // 耗时任务
}
handResult(result) // 切回到主线程执行
}
这里需要注意的是: Dispatchers.Main 是 Android 里面特有的,如果是java程序里面是用则会抛出异常。
创建协程的三种方式
使用 runBlocking 顶层函数创建:
runBlocking {
...
}
使用 GlobalScope 单例对象创建
GlobalScope.launch {
...
}
自行通过 CoroutineContext 创建一个 CoroutineScope 对象
val coroutineScope = CoroutineScope(context)
coroutineScope.launch {
...
}
方法一通常适用于单元测试的场景,而业务开发中不会用到这种方法,因为它是线程阻塞的。
方法二和使用 runBlocking 的区别在于不会阻塞线程。但在 Android 开发中同样不推荐这种用法,因为它的生命周期会只受整个应用程序的生命周期限制,且不能取消。
方法三是比较推荐的使用方法,我们可以通过 context 参数去管理和控制协程的生命周期(这里的 context 和 Android 里的不是一个东西,是一个更通用的概念,会有一个 Android 平台的封装来配合使用)。
等待一个作业
先看一个示例:
fun main() = runBlocking {
launch {
delay(100)
println("hello")
delay(300)
println("world")
}
println("test1")
println("test2")
}
执行结果如下:
test1
test2
hello
world
我们启动了一个协程之后,可以保持对它的引用,显示地等待它执行结束,注意这里的等待是非阻塞的,不会将当前线程挂起。
fun main() = runBlocking {
val job = launch {
delay(100)
println("hello")
delay(300)
println("world")
}
println("test1")
job.join()
println("test2")
}
输出结果:
test1
hello
world
test2
类比 java 线程,也有 join 方法。但是线程是操作系统界别的,在某些 cpu 上,可能 join 方法不生效。
协程的取消
与线程类比,java 线程其实没有提供任何机制来安全地终止线程。
Thread 类提供了一个方法 interrupt() 方法,用于中断线程的执行。调用interrupt()方法并不意味着立即停止目标线程正在进行的工作,而只是传递了请求中断的消息。然后由线程在下一个合适的时机中断自己。
但是协程提供了一个 cancel() 方法来取消作业。
fun main() = runBlocking {
val job = launch {
repeat(1000) { i ->
println("job: test $i ...")
delay(500L)
}
}
delay(1300L) // 延迟一段时间
println("main: ready to cancel!")
job.cancel() // 取消该作业
job.join() // 等待作业执行结束
println("main: Now cancel.")
}
输出结果:
job: test 0 ...
job: test 1 ...
job: test 2 ...
main: ready to cancel!
main: Now cancel.
也可以使用函数 cancelAndJoin, 它合并了对 cancel 以及 join 的调用。
问题:
如果先调用 job.join() 后调用 job.cancel() 是是什么情况?
取消是协作的
协程并不是一定能取消,协程的取消是协作的。一段协程代码必须协作才能被取消。
所有 kotlinx.coroutines 中的挂起函数都是 可被取消的 。它们检查协程的取消, 并在取消时抛出 CancellationException。
如果协程正在执行计算任务,并且没有检查取消的话,那么它是不能被取消的。
fun main() = runBlocking {
val startTime = System.currentTimeMillis()
val job = launch(Dispatchers.Default) {
var nextPrintTime = startTime
var i = 0
while (i < 5) { // 一个执行计算的循环,只是为了占用 CPU
// 每秒打印消息两次
if (System.currentTimeMillis() >= nextPrintTime) {
println("job: hello ${i++} ...")
nextPrintTime += 500L
}
}
}
delay(1300L) // 等待一段时间
println("main: ready to cancel!")
job.cancelAndJoin() // 取消一个作业并且等待它结束
println("main: Now cancel.")
}
此时的打印结果:
job: hello 0 ...
job: hello 1 ...
job: hello 2 ...
main: ready to cancel!
job: hello 3 ...
job: hello 4 ...
main: Now cancel.
可见协程并没有被取消。为了能真正停止协程工作,我们需要定期检查协程是否处于 active 状态。
检查 job 状态
一种方法是在 while(i<5) 中添加检查协程状态的代码
代码如下:
while (i < 5 && isActive)
这样意味着只有当协程处于 active 状态时,我们工作的才会执行。
另一种方法使用协程标准库中的函数 ensureActive(), 它的实现是这样的:
publc fun Job.ensureActive(): Unit {
if (!isActive) throw getCancellationException()
}
代码如下:
while (i < 5) { // 一个执行计算的循环,只是为了占用 CPU
ensureActive()
...
}
ensureActive() 在协程不在 active 状态时会立即抛出异常。
使用 yield()
yield() 和 ensureActive 使用方式一样。
yield 会进行的第一个工作就是检查任务是否完成,如果 Job 已经完成的话,就会抛出 CancellationException 来结束协程。yield 应该在定时检查中最先被调用。
while (i < 5) { // 一个执行计算的循环,只是为了占用 CPU
yield()
...
}
等待协程的执行的结果
对于无返回值的的协程使用 launch 函数创建,如果需要返回值,则通过 async 函数创建。
使用 async 方法启动 Deferred (也是一种 job), 可以调用它的 await() 方法获取执行的结果。
形如下面代码:
val asyncDeferred = async {
...
}
val result = asyncDeferred.await()
deferred 也是可以取消的,对于已经取消的 deferred 调用 await() 方法,会抛出
JobCancellationException 异常。
同理,在 deferred.await 之后调用 deferred.cancel(), 那么什么都不会发生,因为任务已经结束了。
关于 async 的具体用法后面异步任务再讲。
协程的异常处理
由于协程被取消时会抛出 CancellationException ,所以我们可以把挂起函数包裹在 try/catch 代码块中,这样就可以在 finally 代码块中进行资源清理操作了。
fun main() = runBlocking {
val job = launch {
try {
delay(100)
println("try...")
} catch (e: Exception) {
println("exception: ${e.message}")
} finally {
println("finally...")
}
}
delay(50)
println("cancel")
job.cancel()
print("Done")
}
结果:
cancel
Doneexception: StandaloneCoroutine was cancelled
finally...
协程的超时
在实践中绝大多数取消一个协程的理由是它有可能超时。 当你手动追踪一个相关 Job 的引用并启动,使用 withTimeout 函数。
fun main() = runBlocking {
withTimeout(300) {
println("start...")
delay(100)
println("progress 1...")
delay(100)
println("progress 2...")
delay(100)
println("progress 3...")
delay(100)
println("progress 4...")
delay(100)
println("progress 5...")
println("end")
}
}
结果:
start...
progress 1...
progress 2...
Exception in thread "main" kotlinx.coroutines.TimeoutCancellationException: Timed out waiting for 300 ms
withTimeout 抛出了 TimeoutCancellationException,它是 CancellationException 的子类。 我们之前没有在控制台上看到堆栈跟踪信息的打印。这是因为在被取消的协程中 CancellationException 被认为是协程执行结束的正常原因。 然而,在这个示例中我们在 main 函数中正确地使用了 withTimeout。如果有必要,我们需要主动 catch 异常进行处理。
当然,还有另一种方式: 使用 withTimeoutOrNull。
withTimeout 是可以由返回值的,执行 withTimeout 函数,会阻塞并等待执行完返回结果或者超时抛出异常。withTimeoutOrNull 用法与 withTimeout 一样,只是在超时后返回 null 。
除了这些Kotlin的基本学习,还有许多要进阶的技术点,如下:参考《Kotlin手册笔记》可以点击查看详情类目。
最后
本文主要是对Kotlin 协程的简单概述,还有对Kotlin 协程的使用运用。更多的Kotlin的技术可以前往主页查看我的更多文章技术。