Kotlin Coroutine(协程): 二、初识协程
@
前言
你还在用 Hanlder + Message? 或者 AsyncTask? 你还在用 Rxjava?
有人说Rxjava和Coroutine是从不同维度解决异步, 并且Rxjava的强大不止于异步问题.
好吧, 管它呢. 让我们拥抱 Coroutine(协程) 吧.
协程概念:
概念? 那些抽象的话术我们就不提了. 有人说协程是轻量级的线程. 有人说它是一种线程管理框架. 而博主更倾向于后者. 博主认为协程的工作是: 手握线程池, 拆分代码块, 挂起与恢复, 规划与调度, 确保任务按照预期执行 .
那协程会解决什么问题呢?:
- 异步任务, 并行任务
- 协程切换对比线程切换, 开销更小
- 解决"回调地狱", 看起来就像写同步代码
导包:
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:1.4.3"
//for Android
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.4.3'
提示:以下是本篇文章正文内容,下面案例可供参考. 由于篇幅有限, 如想要更详细的测试例子, 请看官网
一、初识协程
我们打印带上线程别名.
概念难懂怎么办? 快把代码敲起来! 敲的多, 懂得快
//打印代码
fun letUsPrintln(title: String){
println("$title Thread_name:${Thread.currentThread().name}")
}
1.runBlocking: 阻塞协程
fun main() {
letUsPrintln("Hello,")
runBlocking { // 这个表达式阻塞了主线程
letUsPrintln("World!")
delay(2000L) // 等待2秒
letUsPrintln("end!")
}
letUsPrintln("我被阻塞了没!")
}
打印结果如下:
Hello, Thread_name:main
World! Thread_name:main
end! Thread_name:main
我被阻塞了没! Thread_name:main
可以发现:
- delay: 一个特殊的 挂起函数 ,它不会造成线程阻塞
- 所有打印均在主线程; 实际是在启动它的线程执行.
- runBlocking{..} 之后的代码, 在runBlocking执行完毕后 才执行
- 所以: runBlocking 是阻塞协程, 它会阻塞主线程, 直到协程执行完毕. 类似于让线程 Thread.sleep(2000)
疑问: runBlocking {} 阻塞的是 主线程? 还是启动它的线程?
我们让 runBlocking {..} 在子线程启动. 并把它们放入 Activity的onCreate函数中. 如果页面不能正常操作, 则表示阻塞了主线程.
runBlocking 不是重点, 我们只帖核心代码, 有兴趣的可以自行测试.
Thread{ runBlocking {
Log...
delay(10000L) // 我们延迟 10 秒来看 UI是否可以操作.
Log...
}}.start()
结论:
- 主线程UI可以正常操作, 所以 runBlocking {} 并非狙击主线程, 而是阻塞启动它的线程
- runBlocking{} 是个顶层协程, 不应放入 launch, async 或 suspend 函数中.
- 因为 runBlocking{} 会阻塞线程, 这样还不如直接执行耗时代码呢. 即便它是为了套子协程, 在Android中阻塞主线程是非常危险的, 所以一般不会直接使用.
2.launch: 创建协程
上代码:
GlobalScope.launch { // 在后台启动一个新的协程并继续;
delay(1000L)
letUsPrintln("World!")
}
letUsPrintln("Hello,") //launch 不是阻塞协程, 后面主线程中的代码会立即执行
runBlocking {
delay(2000L) // 我们阻塞主线程 2 秒来保证 JVM 的存活
letUsPrintln("end!")
}
打印结果:
Hello, Thread_name:main
World! Thread_name:DefaultDispatcher-worker-1
end! Thread_name:main
可以看出:
- World! 是从子线程打印. GlobalScope.launch 默认是子线程执行.
- launch 并没有阻塞主线程.
因为 launch 不阻塞线程, 它不会阻止JVM退出. 我们用了 runBlocking {} 阻止JVM退出.
而协程有个机制, 父协程会等待所有子协程执行完毕,才会退出.
在协程中创建的协程, 都算子协程. 除了 GlobalScope.launch, 它是顶级协程.
所以我们从 runBlocking 中创建子协程, 等待其执行完毕.
fun main() = runBlocking {
launch { // 启动一个新协程, 这是 this.launch
letUsPrintln("World!")
delay(1000L)
letUsPrintln("end!")
}
letUsPrintln("Hello,")
}
打印结果:
Hello, Thread_name:main
World! Thread_name:main
end! Thread_name:main
我们发现, "World!" 也是在主线程打印, 但是先打印了 Hello, 这是因为此处 launch代码块 在主线程运行, 它需要等待主线程空闲. 博主猜测,协程是将要执行的代码以类似消息的方式, 发送到 线程的任务队列中. 类似于 Handler 消息机制.
3.Job
launch 会返回一个 Job对象
public fun CoroutineScope.launch(
context: CoroutineContext = EmptyCoroutineContext,
start: CoroutineStart = CoroutineStart.DEFAULT,
block: suspend CoroutineScope.() -> Unit
): Job {
它的方法有:
函数 | 用法 |
---|---|
join() | 挂起当前协程, 等待 job 协程执行结束 |
cancel() | 取消协程 |
cancelAndJoin() | 取消协程并等待结束. 协程被取消, 但不一定立即结束, 或许还有收尾工作 |
它的参数有: | |
参数 | 意义 |
-- | -- |
isActive | 知否正在运行 |
isCompleted | 是否运行完成 |
isCancelled | 是否已取消 |
它的生命周期, 及参数状态对照如下:
* | **State** | [isActive] | [isCompleted] | [isCancelled] |
* | -------------------------------- | ---------- | ------------- | ------------- |
* | _New_ (optional initial state) | `false` | `false` | `false` |
* | _Active_ (default initial state) | `true` | `false` | `false` |
* | _Completing_ (transient state) | `true` | `false` | `false` |
* | _Cancelling_ (transient state) | `false` | `false` | `true` |
* | _Cancelled_ (final state) | `false` | `true` | `true` |
* | _Completed_ (final state) | `false` | `true` | `false` |
4.coroutineScope
它会创建一个协程作用域并且在所有已启动子协程执行完毕之前不会结束;
下面是一个官方示例
runBlocking{
launch {
delay(200L)
letUsPrintln("Task from runBlocking") // 2. 200 delay launch 不阻塞
}
coroutineScope { // 创建一个协程作用域
launch {
delay(500L)
letUsPrintln("Task from nested launch")
}
delay(100L)
letUsPrintln("Task from coroutine scope") // 1. 100 delay launch 不阻塞
}
letUsPrintln("Coroutine scope is over") // 4. 500 delay coroutineScope
}
执行结果:
Task from coroutine scope Thread_name:main
Task from runBlocking Thread_name:main
Task from nested launch Thread_name:main
Coroutine scope is over Thread_name:main
如果把 coroutineScope 换成 launch. 在100ms之前,上方协程都会因 delay() 进入挂起状态. 所以末尾 over 会最早执行. 但 coroutineScope 会等待作用域及其所有子协程执行结束. 相当于job.join()了, 并且当它内部异常时, 作用域内其他子协程将被取消
5.协程取消
我们已知 job.cancel(), job.cancelAndJoin() 可以取消协程执行. 这样协程会立刻结束吗?
还记得 Thread 类的 stop(), interrupt() 函数吗; 协程终止是否也需要代码配合呢?
下面是一个官方例子:
val startTime = System.currentTimeMillis()
val job = launch(Dispatchers.Default) { // this: CoroutineScope
var nextPrintTime = startTime
var i = 0
while (i < 5) { // 一个执行计算的循环,只是为了占用 CPU
// 每秒打印消息两次
if (System.currentTimeMillis() >= nextPrintTime) {
println("job: I'm sleeping ${i++} ...")
nextPrintTime += 500L
}
}
}
delay(1300L) // 等待一段时间
println("main: I'm tired of waiting!")
job.cancelAndJoin() // 取消一个作业并且等待它结束
println("main: Now I can quit.")
打印结果如下:
job: I'm sleeping 0 ...
job: I'm sleeping 1 ...
job: I'm sleeping 2 ...
main: I'm tired of waiting!
job: I'm sleeping 3 ...
job: I'm sleeping 4 ...
main: Now I can quit.
可以看到, cancel() 执行后, 带有 while 循环的协程仍在运行.
那我们应当如何停止协程呢? 有两种方式:
- 1.定期调用挂起函数来检查取消。 例如: delay(), yield(), job.join() 等
- 2.显式的检查取消状态。
我们先看第二种: 使用 isActive
只需将前一个例子中的 while (i < 5) 替换为 while (isActive) 并重新运行。打印结果如下:
job: I'm sleeping 0 ...
job: I'm sleeping 1 ...
job: I'm sleeping 2 ...
main: I'm tired of waiting!
main: Now I can quit.
isActive: 它是 CoroutineScope 的扩展属性, 等同于 coroutineContext[Job]?.isActive
public val CoroutineScope.isActive: Boolean
get() = coroutineContext[Job]?.isActive ?: true
再看第一种: 循环中使用 delay();
runBlocking {
val job = launch {
repeat(1000) { i ->
delay(500L)
letUsPrintln("job: I'm sleeping $i ...")
}
}
delay(1300L) // 延迟一段时间
letUsPrintln("main: I'm tired of waiting!")
job.cancelAndJoin() // 取消并等待结束
letUsPrintln("main: Now I can quit.")
}
打印结果如下:
job: I'm sleeping 0 ... name:main
job: I'm sleeping 1 ... name:main
main: I'm tired of waiting! name:main
main: Now I can quit. name:main
所有 kotlinx.coroutines 中的挂起函数都是可被取消的 。它们检查协程的取消,并在取消时抛出 CancellationException。
所以: 自己写的 suspend 函数是不行的..;
有的时候, 协程结束时, 我们需要释放资源:
通常我们用 try {…} finally {…} 来捕获异常;
val job = launch {
try {
repeat(1000) { i ->
println("job: I'm sleeping $i ...")
delay(500L)
}
} finally {
println("job: I'm running finally")
}
}
delay(1300L) // 延迟一段时间
println("main: I'm tired of waiting!")
job.cancelAndJoin() // 取消该作业并且等待它结束
println("main: Now I can quit.")
结果如下:
job: I'm sleeping 0 ...
job: I'm sleeping 1 ...
job: I'm sleeping 2 ...
main: I'm tired of waiting!
job: I'm running finally
main: Now I can quit.
6.协程超时
在实践中绝大多数取消一个协程的理由是它有可能超时。
withTimeout(1300L){...}
withTimeout 是一个挂起函数, 需要在协程中执行. 超时会抛出 TimeoutCancellationException 异常, 它是 CancellationException 的子类。 CancellationException 被认为是协程执行结束的正常原因。因此没有打印堆栈跟踪信息.
val result = withTimeoutOrNull(1300L)
withTimeoutOrNull 当超时时会返回 null, 来进行超时操作,从而替代抛出一个异常;
7.async 并行任务
async: 启动一个协程. 它的返回值是Deferred对象, 继承自 Job; 比Job多了函数 await(), 等待执行结果. 结果及异常信息会包装在 Deferred 对象中
先来两个挂起函数:
suspend fun doSomethingUsefulOne(): Int {
delay(1000L) // 假设我们在这里做了一些有用的事
return 13
}
suspend fun doSomethingUsefulTwo(): Int {
delay(1000L) // 假设我们在这里也做了一些有用的事
return 29
}
如果这两个任务先后执行, 将会消耗至少2秒的时间. 此时 async 派上了用场.
runBlocking {
val one = async { doSomethingUsefulOne() }
val two = async { doSomethingUsefulTwo() }
println("The answer is ${one.await() + two.await()}")
}
此时, 总的执行时间约为 1秒.
结构化并发:
suspend fun concurrentSum(): Int = coroutineScope {
val one = async { doSomethingUsefulOne() }
val two = async { doSomethingUsefulTwo() }
one.await() + two.await()
}
这种情况下,如果在 concurrentSum 函数内部发生了错误,并且它抛出了一个异常, 所有在作用域中启动的协程都会被取消。
8.调度器
所有的协程构建器诸如 launch 和 async 接收一个可选的 CoroutineContext 参数,它可以被用来显式的为一个新协程或其它上下文元素指定一个调度器。
几种调度器如下:
调度器 | 意义 |
---|---|
不指定 | 它从启动了它的 CoroutineScope 中承袭了上下文 |
Dispatchers.Main | 用于Android. 在UI线程中执行 |
Dispatchers.IO | 子线程, 适合执行磁盘或网络 I/O操作 |
Dispatchers.Default | 子线程,适合 执行 cpu 密集型的工作 |
Dispatchers.Unconfined | 从当前线程直接执行, 直到第一个挂起点 |
还记得这段代码吗:
runBlocking {
launch { // 启动一个新协程, 这是 this.launch
letUsPrintln("World!")
delay(1000L)
letUsPrintln("end!")
}
letUsPrintln("Hello,")
}
Hello 的打印 早于 World; 原因是 launch 代码块需要等待线程空闲下来才能执行. 假设这里是个耗时任务, 那 launch 也必须得等. 我们改用调度器 Dispatchers.Unconfined
//其他代码一致
launch(Dispatchers.Unconfined) {...}
打印结果如下:
World! Thread_name:main
Hello, Thread_name:main
end! Thread_name:kotlinx.coroutines.DefaultExecutor
Dispatchers.Unconfined: 可以理解为, 我在开启协程前, 把前面一段代码先执行掉. 前面一段就是指的从开始到第一个协程挂起点. 博主想, 那我干脆写协程外面不行吗? 好像是可以. 所以 Unconfined 的使用场景是?
9.withContext
不创建新的协程,在当前协程上运行代码块并返回结果. 一般用来切换执行线程.
runBlocking {
letUsPrintln("start!-主线程")
withContext(Dispatchers.IO) { // 启动一个新协程, 这是 this.launch
delay(1000L)
letUsPrintln("111-子线程!")
}
letUsPrintln("end!-主线程")
}
运行结果如下:
start!-主线程 Thread_name:main
111-子线程! Thread_name:DefaultDispatcher-worker-1
end!-主线程 Thread_name:main
它只改变代码块的执行线程, 完事还会切换回来.
总结
先汇总下注意点:
- GlobalScope 生命周期受整个进程限制, 进程退出才会自动结束. 它不会使进程保活, 像一个守护线程
- 一个线程可以有多个等待执行的协程, 它们不像多线程争抢cpu那样, 它们是排队执行.
- 当然也只有主线程会出现这种情况. 子线程不够用,则可能扩充线程池了
博主想象的协程样子:
- 手握线程池: 它就像一个工头, 手底下一堆人, 谁闲着了就安排上活, 人不够咱还可以招.
- 拆分代码块. 怎么拆? 按挂起点拆, 前后的代码, 必定由单个线程一口气完成. 所以咱就按挂起点拆.
- 挂起与恢复. 代码块已经拆好了, 第一块已经排到了线程任务队列了, 那我就等着呗(挂起), 等它执行完了, 再把下一块安排上, 如果有delay, 那我就定个闹铃眯一觉, 完事再安排(恢复)
- 规划与调度. 给线程池摇人儿啊, 摇到人就安排上活.