好文参考:
https://juejin.cn/post/6893809019425816584
https://juejin.cn/entry/6844903854245412871
1.概念
Essentially, coroutines are light-weight threads.
协程是轻量级的线程。线程是由系统调度的,线程切换或线程阻塞的开销都比较大。而协程依赖于线程,但是协程挂起时不需要阻塞线程,几乎是无代价的,协程是由开发者控制的。所以协程也像用户态的线程,非常轻量级,一个线程中可以创建任意个协程。
2.协程是什么?
协程是一种非抢占式或者说协作式的计算机程序并发调度。
3.协程的作用?
协程可以使用阻塞的方式写出非阻塞式的代码,解决并发中常见的回调地狱,这是其最大的优点。
其他优点:
轻量:可以在单个线程上运行多个协程,因为协程支持挂起,不会使正在运行协程的线程阻塞。挂起比阻塞节省内存,且支持多个并行操作。
内存泄露更少:使用结构化并发机制在一个作用域内执行多个操作。
内置取消支持:取消功能会自动通过正在运行的协程层次结构传播。
Jetpack集成:许多 Jetpack 库都包含提供全面协程支持的扩展。某些库还提供自己的协程作用域,用于结构化并发。
4.Android端依赖的框架
dependencies {
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:1.4.1"
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.4.1'
}
下面例子log函数定义
val dateFormat = SimpleDateFormat("HH:mm:ss:SSS")
val now = {
dateFormat.format(Date(System.currentTimeMillis()))
}
fun log(msg: Any?) = println("${now()} [${Thread.currentThread().name}] $msg")
5.简单启动一个协程
suspend fun main() {
val launch = GlobalScope.launch {
log("666")
}
launch.join()
}
13:29:34:241 [DefaultDispatcher-worker-1] 666
suspend是协程的关键字,每一个被suspend修饰的方法都必须在另一个suspend函数或者Coroutine协程程序中进行调用。
启动协程的 launch方法需要三个参数:上下文、启动模式、协程体,这个协程体可以理解为Thread类中的run方法。
public fun CoroutineScope.launch(
context: CoroutineContext = EmptyCoroutineContext,
start: CoroutineStart = CoroutineStart.DEFAULT,
block: suspend CoroutineScope.() -> Unit
): Job {
val newContext = newCoroutineContext(context)
val coroutine = if (start.isLazy)
LazyStandaloneCoroutine(newContext, block) else
StandaloneCoroutine(newContext, active = true)
coroutine.start(start, coroutine, block)
return coroutine
}
5.1协程的启动模式
public enum class CoroutineStart {
DEFAULT,
LAZY,
ATOMIC,
UNDISPATCHED;
}
启动模式 | 功能特性 |
DEFAULT | 立即开启调度协程体 |
LAZY | 只有需要(start/join/await)时才开始调度 |
ATOMIC | 立即开始调度,且在第一个挂起点前不能被取消 |
UNDISPATCHED | 立即在当前线程执行协程体,直到遇到第一个挂起点(后面取决于调度器) |
5.1.1DEFAULT
DEFAULT 是饿汉式启动,launch 调用后,会立即进入待调度状态,一旦调度器 OK 就可以开始执行。
private suspend fun test2() {
log(1)
val job = GlobalScope.launch {
log(2)
}
log(3)
job.join()
log(4)
}
14:07:32:821 [main] 1
14:07:32:874 [main] 3
14:07:32:875 [DefaultDispatcher-worker-1] 2
14:07:32:880 [main] 4
上面的代码采用的是默认的启动模式,并且也没有指定调度器,所以调度器也是默认的。在JVM上默认调度器的实现是开一个线程池。
5.1.2 LAZY
LAZY 是懒汉式启动,launch 后并不会有任何调度行为,协程体也自然不会进入执行状态,直到我们需要它执行的时候。 launch 调用后会返回一个 Job 实例,对于这种情况,我们可以:
- 调用 Job.start,主动触发协程的调度执行
- 调用 Job.join,隐式的触发协程的调度执行
- 通过 await 来表达对 Deferred 的需要。这个行为与 Thread.join 不一样,后者如果没有启动的话,调用 join 不会有任何作用。
private suspend fun test3() {
log(1)
val job = GlobalScope.launch(start = CoroutineStart.LAZY) {
log(2)
}
log(3)
job.start()//job.join()
log(4)
Thread.sleep(2000)
}
14:25:08:148 [main] 1
14:25:08:178 [main] 3
14:25:08:204 [main] 4
14:25:08:205 [DefaultDispatcher-worker-1] 2
private suspend fun test4() {
log(1)
val job = GlobalScope.launch(start = CoroutineStart.LAZY) {
log(2)
}
log(3)
job.join()
log(4)
}
14:28:04:859 [main] 1
14:28:04:884 [main] 3
14:28:04:910 [DefaultDispatcher-worker-1] 2
14:28:04:913 [main] 4
private suspend fun test5() {
log(1)
val async = GlobalScope.async(start = CoroutineStart.LAZY) {
log(2)
}
log(3)
async.await()
log(4)
}
14:33:40:897 [main] 1
14:33:40:924 [main] 3
14:33:40:952 [DefaultDispatcher-worker-1] 2
14:33:40:960 [main] 4
5.1.3 ATOMIC
ATOMIC 只有涉及 cancel 的时候才有意义(只有遇到第一个挂起点才可以被cancel),cancel 后协程会被取消掉。调用 cancel 的时机不同,结果也是有差异的,例如协程调度之前、开始调度但尚未执行、已经开始执行、执行完毕等等。
private suspend fun test6() {
log(1)
val job = GlobalScope.launch(start = CoroutineStart.ATOMIC) {
log(2)
delay(1000)
log(3)
}
job.cancel()
log(4)
job.join()
}
14:44:36:853 [main] 1
14:44:36:910 [DefaultDispatcher-worker-1] 2
14:44:36:912 [main] 4
在 2 和 3 之间加了一个 delay,delay 会使得协程体的执行被挂起,1000ms 之后再次调度后面的部分,因此 3 会在 2 执行之后 1000ms 时输出。对于 ATOMIC 模式,它一定会被启动,实际上在遇到第一个挂起点之前,它的执行是不会停止的,而 delay 是一个 suspend 函数,这时我们的协程迎来了第一个挂起点,恰好 delay 是支持 cancel 的,因此后面的 3 将不会被打印。协程的 cancel 某种意义上更像线程的 interrupt。
5.1.4 UNDISPATCHED
协程在这种模式下会直接开始在当前线程下执行,直到第一个挂起点,(在第一个挂起点之前不调度,一直在原有的线程上面立即执行)这听起来有点儿像前面的 ATOMIC,不同之处在于 UNDISPATCHED 不经过任何调度器即开始执行协程体。当然遇到挂起点之后的执行就取决于挂起点本身的逻辑以及上下文当中的调度器了。
private suspend fun test7() {
log(1)
val job = GlobalScope.launch(start = CoroutineStart.UNDISPATCHED) {
log(2)
delay(100)
log(3)
}
log(4)
job.join()
log(5)
}
15:00:30:713 [main] 1
15:00:30:748 [main] 2
15:00:30:759 [main] 4
15:00:30:875 [DefaultDispatcher-worker-1] 3
15:00:30:877 [DefaultDispatcher-worker-1] 5
协程启动后会立即在当前线程执行,因此 1、2 会连续在同一线程中执行,delay 是挂起点,因此 3 会等 100ms 后再次调度,这时候 4 执行,join 要求等待协程执行完,因此等 3 输出后再执行 5。