@

前言

你还在用 Hanlder + Message? 或者 AsyncTask? 你还在用 Rxjava?

有人说RxjavaCoroutine是从不同维度解决异步, 并且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那样, 它们是排队执行.
  • 当然也只有主线程会出现这种情况. 子线程不够用,则可能扩充线程池了

博主想象的协程样子:

  1. 手握线程池: 它就像一个工头, 手底下一堆人, 谁闲着了就安排上活, 人不够咱还可以招.
  2. 拆分代码块. 怎么拆? 按挂起点拆, 前后的代码, 必定由单个线程一口气完成. 所以咱就按挂起点拆.
  3. 挂起与恢复. 代码块已经拆好了, 第一块已经排到了线程任务队列了, 那我就等着呗(挂起), 等它执行完了, 再把下一块安排上, 如果有delay, 那我就定个闹铃眯一觉, 完事再安排(恢复)
  4. 规划与调度. 给线程池摇人儿啊, 摇到人就安排上活.

上一篇: Kotlin Coroutine(协程): 一、样例

下一篇: Kotlin Coroutine(协程): 三、了解协程

Kotlin Coroutine(协程): 二、初识协程的更多相关文章

  1. Kotlin Coroutine(协程): 一、样例

    @ 目录 前言 一.直接上例子 1.延时任务. 2.异步任务 3.并行任务: 4.定时任务: 总结 前言 你还在用 Hanlder + Message? 或者 AsyncTask? 你还在用 Rxja ...

  2. Kotlin Coroutine(协程): 三、了解协程

    @ 目录 前言 一.协程上下文 1.调度器 2.给协程起名 3.局部变量 二.启动模式 CoroutineStart 三.异常处理 1.异常测试 2.CoroutineExceptionHandler ...

  3. PHP下的异步尝试二:初识协程

    PHP下的异步尝试系列 如果你还不太了解PHP下的生成器,你可以根据下面目录翻阅 PHP下的异步尝试一:初识生成器 PHP下的异步尝试二:初识协程 PHP下的异步尝试三:协程的PHP版thunkify ...

  4. Python协程与Go协程的区别二

    写在前面 世界是复杂的,每一种思想都是为了解决某些现实问题而简化成的模型,想解决就得先面对,面对就需要选择角度,角度决定了模型的质量, 喜欢此UP主汤质看本质的哲学科普,其中简洁又不失细节的介绍了人类 ...

  5. 深入分析 Java、Kotlin、Go 的线程和协程

    前言 协程是什么 协程的好处 进程 进程是什么 进程组成 进程特征 线程 线程是什么 线程组成 任务调度 进程与线程的区别 线程的实现模型 一对一模型 多对一模型 多对多模型 线程的"并发& ...

  6. python进阶(二) 多进程+协程

    我们大多数的时候使用多线程,以及多进程,但是python中由于GIL全局解释器锁的原因,python的多线程并没有真的实现 实际上,python在执行多线程的时候,是通过GIL锁,进行上下文切换线程执 ...

  7. Python自动化开发 -进程、线程和协程(二)

    本节内容 一.线程进程介绍 二. 线程 1.线程基本使用 (Threading) 2.线程锁(Lock.RLock) 3.信号量(Semaphore) 4.事件(event) 5.条件(Conditi ...

  8. tornado用户指引(二)------------tornado协程实现原理和使用(一)

    摘要:Tornado建议使用协程来实现异步调用.协程使用python的yield关键字来继续或者暂停执行,而不用编写大量的callback函数来实现.(在linux基于epoll的异步调用中,我们需要 ...

  9. Swoole 协程与 Go 协程的区别

    Swoole 协程与 Go 协程的区别 进程.线程.协程的概念 进程是什么? 进程就是应用程序的启动实例. 例如:打开一个软件,就是开启了一个进程. 进程拥有代码和打开的文件资源,数据资源,独立的内存 ...

随机推荐

  1. Qt 设置窗体透明

    一.前言 在音频开发中,窗体多半为半透明.圆角窗体,如下为Qt 5.5 VS2013实现半透明方法总结. 二.半透明方法设置 1.窗体及子控件都设置为半透明 1)setWindowOpacity(0. ...

  2. .Net RabbitMQ实战指南——客户端开发

    开发中关键的Class和Interface有Channel.Connection.ConnectionFactory.Consumer等,与RabbitMQ相关的开发工作,基本上是围绕Connecti ...

  3. Docker_Swarm集群系统

    Docker_Swarm集群系统 一.Docker Swarm 介绍 实践中会发现,生产环境中使用单个 Docker 节点是远远不够的,搭建 Docker 集群势在必行.然而,面对 Kubernete ...

  4. Python分析离散心率信号(中)

    Python分析离散心率信号(中) 一些理论和背景 心率信号不仅包含有关心脏的信息,还包含有关呼吸,短期血压调节,体温调节和荷尔蒙血压调节(长期)的信息.也(尽管不总是始终如一)与精神努力相关联,这并 ...

  5. MySQL必知必会复习笔记(1)

    MySQL必知必会笔记(一) MySQL必知必会是一本很优秀的MySQL教程书,并且相当精简,在日常中甚至能当成一本工作手册来查看.本系列笔记记录的是:1.自己记得不够牢的代码:2.自己觉得很重要的代 ...

  6. centos 7 查看磁盘使用情况

    1.查询系统整体磁盘使用情况 df -h [root@hadoop100 aubunt]# df -h 文件系统 容量 已用 可用 已用% 挂载点 /dev/mapper/centos-root 17 ...

  7. 彻底解决Spring mvc中时间类型的转换和序列化问题

    在使用Spring mvc 进行开发时我们经常遇到前端传来的某种格式的时间字符串无法用java8时间包下的具体类型参数来直接接收.同时还有一系列的序列化 .反序列化问题,在返回前端带时间类型的同样会出 ...

  8. Mysql慢SQL分析及优化

    为何对慢SQL进行治理 从数据库角度看:每个SQL执行都需要消耗一定I/O资源,SQL执行的快慢,决定资源被占用时间的长短.假设总资源是100,有一条慢SQL占用了30的资源共计1分钟.那么在这1分钟 ...

  9. 「模拟8.23」one递推,约瑟夫

    前置芝士约瑟夫问题 这样大概就是板子问题了 考场的树状数组+二分的60分暴力??? 1 #include<bits/stdc++.h> 2 #define int long long 3 ...

  10. StringUtils中的常量

    //空格字符串 public static final String SPACE = " "; //空字符串 public static final String EMPTY = ...