Kotlin协程基础知识, 一篇搞懂.html
Coroutines(协程), 计算机程序组件, 经过容许任务挂起和恢复执行, 来支持非抢占式的多任务. (见Wiki).android
协程主要是为了异步, 非阻塞的代码. 这个概念并非Kotlin特有的, Go, Python等多个语言中都有支持.git
Kotlin中用协程来作异步和非阻塞任务, 主要优势是代码可读性好, 不用回调函数. (用协程写的异步代码乍一看很像同步代码.)github
Kotlin对协程的支持是在语言级别的, 在标准库中只提供了最低程度的APIs, 而后把不少功能都代理到库中.promise
Kotlin中只加了suspend
做为关键字. async
和await
不是Kotlin的关键字, 也不是标准库的一部分.安全
比起futures和promises, kotlin中suspending function
的概念为异步操做提供了一种更安全和不易出错的抽象.bash
kotlinx.coroutines
是协程的库, 为了使用它的核心功能, 项目须要增长kotlinx-coroutines-core
的依赖.网络
先上一段官方的demo:架构
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
fun main() {
GlobalScope.launch { // launch a new coroutine in background and continue
delay(1000L) // non-blocking delay for 1 second (default time unit is ms)
println("World!") // print after delay
}
println("Hello,") // main thread continues while coroutine is delayed
Thread.sleep(2000L) // block main thread for 2 seconds to keep JVM alive
}
复制代码
这段代码的输出: 先打印Hello, 延迟1s以后, 打印World.并发
对这段代码的解释:
launch
开始了一个计算, 这个计算是可挂起的(suspendable), 它在计算过程当中, 释放了底层的线程, 当协程执行完成, 就会恢复(resume).
这种可挂起的计算就叫作一个协程(coroutine). 因此咱们能够简单地说launch
开始了一个新的协程.
注意, 主线程须要等待协程结束, 若是注释掉最后一行的Thread.sleep(2000L)
, 则只打印Hello, 没有World.
coroutine(协程)能够理解为轻量级的线程. 多个协程能够并行运行, 互相等待, 互相通讯. 协程和线程的最大区别就是协程很是轻量(cheap), 咱们能够建立成千上万个协程而没必要考虑性能.
协程是运行在线程上能够被挂起的运算. 能够被挂起, 意味着运算能够被暂停, 从线程移除, 存储在内存里. 此时, 线程就能够自由作其余事情. 当计算准备好继续进行时, 它会返回线程(但不必定要是同一个线程).
默认状况下, 协程运行在一个共享的线程池里, 线程仍是存在的, 只是一个线程能够运行多个协程, 因此线程不必太多.
在上面的代码中加上线程的名字:
fun main() {
GlobalScope.launch {
// launch a new coroutine in background and continue
delay(1000L) // non-blocking delay for 1 second (default time unit is ms)
println("World! + ${Thread.currentThread().name}") // print after delay
}
println("Hello, + ${Thread.currentThread().name}") // main thread continues while coroutine is delayed
Thread.sleep(2000L) // block main thread for 2 seconds to keep JVM alive
}
复制代码
能够在IDE的Edit Configurations中设置VM options: -Dkotlinx.coroutines.debug
, 运行程序, 会在log中打印出代码运行的协程信息:
Hello, + main
World! + DefaultDispatcher-worker-1 @coroutine#1
复制代码
上面例子中的delay
方法是一个suspend function
. delay()
和Thread.sleep()
的区别是: delay()
方法能够在不阻塞线程的状况下延迟协程. (It doesn't block a thread, but only suspends the coroutine itself). 而Thread.sleep()
则阻塞了当前线程.
因此, suspend的意思就是协程做用域被挂起了, 可是当前线程中协程做用域以外的代码不被阻塞.
若是把GlobalScope.launch
替换为thread
, delay方法下面会出现红线报错:
Suspend functions are only allowed to be called from a coroutine or another suspend function
复制代码
suspend方法只能在协程或者另外一个suspend方法中被调用.
在协程等待的过程当中, 线程会返回线程池, 当协程等待结束, 协程会在线程池中一个空闲的线程上恢复. (The thread is returned to the pool while the coroutine is waiting, and when the waiting is done, the coroutine resumes on a free thread in the pool.)
启动一个新的协程, 经常使用的主要有如下几种方式:
launch
async
runBlocking
它们被称为coroutine builders
. 不一样的库能够定义其余更多的构建方式.
runBlocking
用来链接阻塞和非阻塞的世界.
runBlocking
能够创建一个阻塞当前线程的协程. 因此它主要被用来在main函数中或者测试中使用, 做为链接函数.
好比前面的例子能够改写成:
fun main() = runBlocking<Unit> {
// start main coroutine
GlobalScope.launch {
// launch a new coroutine in background and continue
delay(1000L)
println("World! + ${Thread.currentThread().name}")
}
println("Hello, + ${Thread.currentThread().name}") // main coroutine continues here immediately
delay(2000L) // delaying for 2 seconds to keep JVM alive
}
复制代码
最后再也不使用Thread.sleep()
, 使用delay()
就能够了. 程序输出:
Hello, + main @coroutine#1
World! + DefaultDispatcher-worker-1 @coroutine#2
复制代码
上面的例子delay了一段时间来等待一个协程结束, 不是一个好的方法.
launch
返回Job
, 表明一个协程, 咱们能够用Job
的join()
方法来显式地等待这个协程结束:
fun main() = runBlocking {
val job = GlobalScope.launch {
// launch a new coroutine and keep a reference to its Job
delay(1000L)
println("World! + ${Thread.currentThread().name}")
}
println("Hello, + ${Thread.currentThread().name}")
job.join() // wait until child coroutine completes
}
复制代码
输出结果和上面是同样的.
Job
还有一个重要的用途是cancel()
, 用于取消再也不须要的协程任务.
async
开启线程, 返回Deferred<T>
, Deferred<T>
是Job
的子类, 有一个await()
函数, 能够返回协程的结果.
await()
也是suspend函数, 只能在协程以内调用.
fun main() = runBlocking {
// @coroutine#1
println(Thread.currentThread().name)
val deferred: Deferred<Int> = async {
// @coroutine#2
loadData()
}
println("waiting..." + Thread.currentThread().name)
println(deferred.await()) // suspend @coroutine#1
}
suspend fun loadData(): Int {
println("loading..." + Thread.currentThread().name)
delay(1000L) // suspend @coroutine#2
println("loaded!" + Thread.currentThread().name)
return 42
}
复制代码
运行结果:
main @coroutine#1
waiting...main @coroutine#1
loading...main @coroutine#2
loaded!main @coroutine#2
42
复制代码
看一下launch
方法的声明:
public fun CoroutineScope.launch(
context: CoroutineContext = EmptyCoroutineContext,
start: CoroutineStart = CoroutineStart.DEFAULT,
block: suspend CoroutineScope.() -> Unit
): Job {
...
}
复制代码
其中有几个相关概念咱们要了解一下.
协程老是在一个context下运行, 类型是接口CoroutineContext
. 协程的context是一个索引集合, 其中包含各类元素, 重要元素就有Job
和dispatcher. Job
表明了这个协程, 那么dispatcher是作什么的呢?
构建协程的coroutine builder: launch
, async
, 都是CoroutineScope
类型的扩展方法. 查看CoroutineScope
接口, 其中含有CoroutineContext
的引用. scope是什么? 有什么做用呢?
下面咱们就来回答这些问题.
Context中的CoroutineDispatcher
能够指定协程运行在什么线程上. 能够是一个指定的线程, 线程池, 或者不限.
看一个例子:
fun main() = runBlocking<Unit> {
launch {
// context of the parent, main runBlocking coroutine
println("main runBlocking : I'm working in thread ${Thread.currentThread().name}")
}
launch(Dispatchers.Unconfined) {
// not confined -- will work with main thread
println("Unconfined : I'm working in thread ${Thread.currentThread().name}")
}
launch(Dispatchers.Default) {
// will get dispatched to DefaultDispatcher
println("Default : I'm working in thread ${Thread.currentThread().name}")
}
launch(newSingleThreadContext("MyOwnThread")) {
// will get its own new thread
println("newSingleThreadContext: I'm working in thread ${Thread.currentThread().name}")
}
}
复制代码
运行后打印出:
Unconfined : I'm working in thread main Default : I'm working in thread DefaultDispatcher-worker-1
newSingleThreadContext: I'm working in thread MyOwnThread main runBlocking : I'm working in thread main
复制代码
API提供了几种选项:
Dispatchers.Default
表明使用JVM上的共享线程池, 其大小由CPU核数决定, 不过即使是单核也有两个线程. 一般用来作CPU密集型工做, 好比排序或复杂计算等.Dispatchers.Main
指定主线程, 用来作UI更新相关的事情. (须要添加依赖, 好比kotlinx-coroutines-android
.) 若是咱们在主线程上启动一个新的协程时, 主线程忙碌, 这个协程也会被挂起, 仅当线程有空时会被恢复执行.Dispatchers.IO
: 采用on-demand建立的线程池, 用于网络或者是读写文件的工做.Dispatchers.Unconfined
: 不指定特定线程, 这是一个特殊的dispatcher.若是不明确指定dispatcher, 协程将会继承它被启动的那个scope的context(其中包含了dispatcher).
在实践中, 更推荐使用外部scope的dispatcher, 由调用方决定上下文. 这样也方便测试.
newSingleThreadContext
建立了一个线程来跑协程, 一个专一的线程算是一种昂贵的资源, 在实际的应用中须要被释放或者存储复用.
切换线程还能够用withContext
, 能够在指定的协程context下运行代码, 挂起直到它结束, 返回结果. 另外一种方式是新启一个协程, 而后用join
明确地挂起等待.
在Android这种UI应用中, 比较常见的作法是, 顶部协程用CoroutineDispatchers.Main
, 当须要在别的线程上作一些事情的时候, 再明确指定一个不一样的dispatcher.
当launch
, async
或runBlocking
开启新协程的时候, 它们自动建立相应的scope. 全部的这些方法都有一个带receiver的lambda参数, 默认的receiver类型是CoroutineScope
.
IDE会提示this: CoroutineScope
:
launch { /* this: CoroutineScope */
}
复制代码
当咱们在runBlocking
, launch
, 或async
的大括号里面再建立一个新的协程的时候, 自动就在这个scope里建立:
fun main() = runBlocking {
/* this: CoroutineScope */
launch { /* ... */ }
// the same as:
this.launch { /* ... */ }
}
复制代码
由于launch
是一个扩展方法, 因此上面例子中默认的receiver是this
. 这个例子中launch
所启动的协程被称做外部协程(runBlocking
启动的协程)的child. 这种"parent-child"的关系经过scope传递: child在parent的scope中启动.
协程的父子关系:
因此, 关于scope目前有两个关键知识点:
CoroutineScope
里.协程的父子关系有如下两个特性:
值得注意的是, 也能够不启动协程就建立一个新的scope. 建立scope能够用工厂方法: MainScope()
或CoroutineScope()
.
coroutineScope()
方法也能够建立scope. 当咱们须要以结构化的方式在suspend函数内部启动新的协程, 咱们建立的新的scope, 自动成为suspend函数被调用的外部scope的child.
因此上面的父子关系, 能够进一步抽象到, 没有parent协程, 由scope来管理其中全部的子协程. (注意: 实际上scope会提供默认job, cancel
操做是由scope中的job支持的.)
Scope在实际应用中解决什么问题呢? 若是咱们的应用中, 有一个对象是有本身的生命周期的, 可是这个对象又不是协程, 好比Android应用中的Activity, 其中启动了一些协程来作异步操做, 更新数据等, 当Activity被销毁的时候须要取消全部的协程, 来避免内存泄漏. 咱们就能够利用CoroutineScope
来作这件事: 建立一个CoroutineScope
对象和activity的生命周期绑定, 或者让activity实现CoroutineScope
接口.
因此, scope的主要做用就是记录全部的协程, 而且能够取消它们.
A CoroutineScope keeps track of all your coroutines, and it can cancel all of the coroutines started in it.
复制代码
这种利用scope将协程结构化组织起来的机制, 被称为"structured concurrency". 好处是:
经过这种结构化的并发模式: 咱们能够在建立top级别的协程时, 指定主要的context一次, 全部嵌套的协程会自动继承这个context, 只在有须要的时候进行修改便可.
GlobalScope
启动的协程都是独立的, 它们的生命只受到application的限制. 即GlobalScope
启动的协程没有parent, 和它被启动时所在的外部的scope没有关系.
launch(Dispatchers.Default) { ... }
和GlobalScope.launch { ... }
用的dispatcher是同样的.
GlobalScope
启动的协程并不会保持进程活跃. 它们就像daemon threads(守护线程)同样, 若是JVM发现没有其余通常的线程, 就会关闭.
第三方博客: