目录:
一. 协程的基本概念
协程就像非常轻量级的线程。线程是由系统调度的,线程切换或线程阻塞的开销都比较大。而协程依赖于线程,但是协程挂起时不需要阻塞线程,几乎是无代价的,协程是由开发者控制的。
二. 从异步编程开始
我们先从一个例子说起,发送一个带有认证的 post 请求,需要以下三个步骤,首先客户端向服务其发送一个得到token的请求,然后构造一个 Post 请求,最后将 Post 请求发出去。这三个请求都是耗时操作,而且请求和请求之间有着依赖的关系。fun requestToken(): Token {
delay(500L) // 模拟请求过程
return token
}
fun createPost(token: Token, item: Item): Post {
delay(500L) // 模拟构造过程
return post
}
fun processPost(post: Post) {
delay(500L) // 模拟请求过程
}
方法一:使用回调的方式
操作2依赖于操作1,所以把操作2作为回调放在操作1的参数内,由操作1决定回调时机。
fun requestTokenAsync(cb: (Token) -> Unit) { ... }
fun createPostAsync(token: Token, item: Item, cb: (Post) -> Unit) { ... }
fun processPost(post: Post) { ... }
fun postItem(item: Item) {
requestTokenAsync { token ->
createPostAsync(token, item) { post ->
processPost(post)
}
}
}
这种多层嵌套的方式比较复杂,而且不方便处理异常情况。
方法二:CompletableFuture
Java 8 引入的 CompletableFuture 可以将多个任务串联起来,可以避免多层嵌套的问题。
可以简单看一下API,具体的使用方法参考文章:
CompletableFuture 使用详解
方法 | 作用 |
---|---|
runAsync | 创建一个异步操作,不支持返回值 |
supplyAsync | 创建一个异步操作,支持返回值 |
whenComplete | 计算结果完成的回调方法 |
exceptionally | 计算结果出现异常的回调方法 |
thenApply | 当一个线程依赖另一个线程时,可以使用 thenApply 方法来把这两个线程串行化。 |
handle | 与thenApply相似,handle还可以处理异常任务 |
thenAccept | 与thenApply相似,但是没有返回值 |
thenRun | 与thenAccept相似,但是得不到上面任务的处理结果 |
thenCombine | 合并任务,有返回值 |
thenAcceptBoth | 合并任务,无返回值 |
applyToEither | 两个任务用哪个结果 |
acceptEither | 谁返回的结果快使用那个结果 |
runAfterEither | 任何一个完成都会执行下一步操作 |
runAfterBoth | 都完成了才会执行下一步操作 |
thenCompose | 允许你对两个 CompletionStage 进行流水线操作,第一个操作完成时,将其结果作为参数传递给第二个操作。 |
知道了API后就可以这么写
fun requestTokenAsync(): CompletableFuture<Token> { ... }
fun createPostAsync(token: Token, item: Item): CompletableFuture<Post> { ... }
fun processPost(post: Post) { ... }
fun postItem(item: Item) {
requestTokenAsync()
.thenCompose { token -> createPostAsync(token, item) }
.thenAccept { post -> processPost(post) }
.exceptionally { e ->
e.printStackTrace()
null
}
}
方法三: RxJava
RxJava的用法跟CompletableFuture链式调用比较类似,这也是比较简洁,比较多人使用的方式:
fun requestToken(): Token { ... }
fun createPost(token: Token, item: Item): Post { ... }
fun processPost(post: Post) { ... }
fun postItem(item: Item) {
Single.fromCallable { requestToken() }
.map { token -> createPost(token, item) }
.subscribe(
{ post -> processPost(post) }, // onSuccess
{ e -> e.printStackTrace() } // onError
)
}
方法四:协程的方式
suspend fun requestToken(): Token { ... } // 挂起函数
suspend fun createPost(token: Token, item: Item): Post { ... } // 挂起函数
fun processPost(post: Post) { ... }
fun postItem(item: Item) {
GlobalScope.launch {
val token = requestToken()
val post = createPost(token, item)
processPost(post)
// 需要异常处理,直接加上 try/catch 语句即可
}
}
协程可以让我们使用顺序的方式去写异步代码,而且并不会阻塞UI线程。
三. 协程的基本概念
1. suspend funtion
我们写的有两个方法是挂起的函数(suspend function)
suspend fun requestToken(): Token { ... }
suspend fun createPost(token: Token, item: Item): Post { ... }
首先要知道的是,挂起函数挂起协程的时候,并不会阻塞线程。
然后一个 suspend function 只能在一个协程或一个 suspend function 中使用,但是suspend function和普通函数使用方法一样,有自己的参数,有自己的返回值,那么为什么要使用suspend funtion呢?
我们可以看到delay函数是一个挂起函数 , Thread.sleep()是一个阻塞函数,如果我们在一个A函数可能会挂起协程,比如调用delay()方法,因为 delay() 是suspend function ,只能在一个协程或一个suspend function中使用,所以A函数也必须是suspend function。所以使用suspend funtion的标准是该函数有无挂起操作。
public suspend fun delay(timeMillis: Long) {
if (timeMillis <= 0) return // don't delay
return suspendCancellableCoroutine sc@ { cont: CancellableContinuation<Unit> ->
cont.context.delay.scheduleResumeAfterDelay(timeMillis, cont)
}
}
2. CoroutineScope
CoroutineScope为协程的作用域,可以管理其域内的所有协程。一个CoroutineScope可以有许多的子scope。
创建子scope的方式有许多种, 常见的方式有:
方法一:使用lauch, async 等builder创建一个新的子协程。
我们来看一下CoroutineScop接口
// 每个Coroutine作用域都有一个Coroutine上下文
public interface CoroutineScope {
// Scope 的 Context
public val coroutineContext: CoroutineContext
}
所以 CoroutineScope 只是定义了一个新 Coroutine 的 coroutineContext,其实每个 coroutine builder(launch
,async) 都是 CoroutineScope 的扩展函数,并且自动的继承了当前 Scope 的 coroutineContext 和取消操作。我们以launch方法为例:
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
}
-
第一个参数 context,默认 launch 所创建的 Coroutine 会自动继承当前 Coroutine 的 context,如果有额外的 conetxt 需要传递给所创建的 Coroutine 则可以通过第一个参数来设置。
-
第二个参数 start 为 CoroutineStart 枚举类型,用来指定 Coroutine 启动的选项。有如下几个取值:
- DEFAULT (默认值)立刻安排执行该Coroutine实例
- LAZY 延迟执行,只有当用到的时候才执行
- ATOMIC 类似 DEFAULT,区别是当Coroutine还没有开始执行的时候无法取消
- UNDISPATCHED 如果使用 Dispatchers.Unconfined dispatcher,则立刻在当前线程执行直到遇到第一个suspension point。然后当 Coroutine 恢复的时候,在继续在 suspension的 context 中设置的 CoroutineDispatcher 中执行。 -
第三个参数 block 为一个 suspending function,这个就是 Coroutine 中要执行的代码块,在实际使用过程中通常使用 lambda 表达式,也称之为 Coroutine 代码块。需要注意的是,这个 block 函数定义为 CoroutineScope 的扩展函数,所以在代码块中可以直接访问 CoroutineScope 对象(也就是 this 对象)
结论:launch方法实际上就是new了一个LazyStandaloneCoroutine协程(isLazy属性为false),协程自动的继承了当前 Scope(this代表的协程scope) 的 coroutineContext 和取消操作。
方法二:使用coroutineScope Api创建新scope:
public suspend fun <R> coroutineScope(block: suspend CoroutineScope.() -> R): R
这个api主要用于方便地创建一个子域(相当于创建一个局部作用域),并且管理域中的所有子协程。注意这个方法只有在所有 block中创建的子协程全部执行完毕后,才会退出。
// print输出的结果顺序将会是 1, 2, 3, 4
coroutineScope {
delay(1000)
println("1")
launch {
delay(6000)
println("3")
}
println("2")
return@coroutineScope
}
println("4")
方法三:继承CoroutineScope.这也是比较推荐的做法,用于处理具有生命周期的对象。
在 Android 环境中,通常每个界面(Activity、Fragment 等)启动的 Coroutine 只在该界面有意义,如果用户在等待 Coroutine 执行的时候退出了这个界面,则再继续执行这个 Coroutine 可能是没必要的。那么我们怎么让activity管理好其内的 Coroutine 呢?
我们来看下面的例子:
class ScopedActivity : Activity(), CoroutineScope {
lateinit var job: Job
// CoroutineScope 的实现
override val coroutineContext: CoroutineContext
get() = Dispatchers.Main + job
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
job = Job()
}
override fun onDestroy() {
super.onDestroy()
// 当 Activity 销毁的时候取消该 Scope 管理的 job。
// 这样在该 Scope 内创建的子 Coroutine 都会被自动的取消。
job.cancel()
}
/*
* 注意 coroutine builder 的 scope, 如果 activity 被销毁了或者该函数内创建的 Coroutine
* 抛出异常了,则所有子 Coroutines 都会被自动取消。不需要手工去取消。
*/
fun loadDataFromUI() = launch { // <- 自动继承当前 activity 的 scope context,所以在 UI 线程执行
val ioData = async(Dispatchers.IO) { // <- launch scope 的扩展函数,指定了 IO dispatcher,所以在 IO 线程运行
// 在这里执行阻塞的 I/O 耗时操作
}
// 和上面的并非 I/O 同时执行的其他操作
val data = ioData.await() // 等待阻塞 I/O 操作的返回结果
draw(data) // 在 UI 线程显示执行的结果
}
}
解释一下这个地方:get() = Dispatchers.Main + job
一个上下文(context)可以是多个上下文的组合。组合的上下文需要是不同的类型。所以,你需要做两件事情:
- 一个 dispatcher: 用于指定协程默认使用的 dispatcher;
- 一个 job: 用于在任何需要的时候取消协程;
操作符号 + 用于组合上下文。如果两种不同类型的上下文相组合,会生成一个组合的上下文(CombinedContext),这个新的上下文会同时拥有被组合上下文的特性。因为:get() = Dispatchers.Main + job
,所以launch方法实际上是在Dispatchers.Main,也就是在UI线程中执行的。
3. CoroutineContext
CoroutineScope 可以理解为一个协程,里面有一个协程的上下文:CoroutineContext,这个协程上下文包含很多该协程的信息,比如:Job, ContinuationInterceptor, CoroutineName 和CoroutineId。在CoroutineContext中,是用map来存这些信息的, map的键是这些类的伴生对象,值是这些类的一个实例,你可以这样子取得context的信息:
val job = context[Job]
val continuationInterceptor = context[ContinuationInterceptor]
4. CoroutineDispatcher
CoroutineDispatcher,协程调度器,决定协程所在的线程或线程池。它可以指定协程运行于特定的一个线程、一个线程池或者不指定任何线程(这样协程就会运行于当前线程)。coroutines-core中 CoroutineDispatcher 有四种标准实现Dispatchers.Default、Dispatchers. IO,Dispatchers.Main 和 Dispatchers.Unconfined,Unconfined 就是不指定线程。
- Dispatchers.Default: 如果创建 Coroutine 的时候没有指定 dispatcher,则一般默认使用这个作为默认值。Default dispatcher 使用一个共享的后台线程池来运行里面的任务。
- Dispatchers. IO: 顾名思义这是用来执行阻塞 IO 操作的,也是用一个共享的线程池来执行里面的任务。根据同时运行的任务数量,在需要的时候会创建额外的线程,当任务执行完毕后会释放不需要的线程。通过系统 property kotlinx.coroutines.io.parallelism 可以配置最多可以创建多少线程,在 Android 环境中我们一般不需要做任何额外配置。
- Dispatchers.Unconfined: 立刻在启动 Coroutine 的线程开始执行该 Coroutine直到遇到第一个 suspension point。也就是说,coroutine builder 函数在遇到第一个 suspension point 的时候才会返回。而 Coroutine 恢复的线程取决于 suspension function 所在的线程。 一般而言我们不使用 Unconfined。
- Dispatchers.Main: 是在 Android 的 UI 线程执行。
- 通过 newSingleThreadContext 和 newFixedThreadPoolContext 函数可以创建在私有的线程池中运行的 Dispatcher。由于创建线程比较消耗系统资源,所以对于临时创建的线程池在使用完毕后需要通过 close 函数来关闭线程池并释放资源。
5. Job 和 Deffered
CoroutineScope.launch 函数返回一个 Job 对象,该对象代表了这个刚刚创建的 Coroutine实例,job 对象有不同的状态(刚创建的状态、活跃的状态、执行完毕的状态、取消状态等),通过这个 job 对象可以控制这个 Coroutine 实例,比如调用 cancel 函数可以取消执行。Job对象持有所有的子job实例,可以取消所有子job的运行。Job的join方法会等待自己以及所有子job的执行, 所以Job给予了CoroutineScope一个管理自己所有子协程的能力。
CoroutineScope.async 函数也是三个参数,参数类型和 launch 一样,唯一的区别是第三个block参数会返回一个值,而 async 函数的返回值为 Deferred 类型。可以通过 Deferred 对象获取异步代码块(block)返回的值。Deferred 继承了 Job,它有个 await() 方法。
// Awaits for completion of this value without blocking a thread and resumes when deferred computation is complete,
// returning the resulting value or throwing the corresponding exception if the deferred was cancelled.
public suspend fun await(): T
6. Coroutine builders
- CoroutineScope.launch : 不阻塞当前线程,在后台创建一个新协程,也可以指定协程调度器,无返回值
- CoroutineScope.async : 在后台创建一个新协程,有返回值
- runBlocking :
创建一个新的协程来阻塞当前线程,直到 runBlocking 代码块执行完成。通常它不会用于协程中,因为在协程中写一个阻塞的代码块实在太别扭,可以通过挂起操作取代。它通常作为一个适配器,将 main 线程转换成一个 main 协程,我们也就持有了一个 main 协程的 coroutineContext 上下文对象,就可以随心所欲用(this)使用 coroutineContext 的扩展方法,随心所欲使用 suspend 方法 ( suspend 方法只能用于 suspend 方法和协程中)。所以 runBlocking 一般用在 test 函数和 main 函数中。
- withContext :
withContext 不会创建一个新的协程,在指定的协程上运行代码块,并挂起该协程直到代码块运行完成。通常是用于切换协程的上下文。
例如:
// 使用 withContext 切换协程,上面的例子就是先在 IO 线程里执行,然后切换到主线程。
GlobalScope.launch(Dispatchers.IO) {
...
withContext(Dispatchers.Main) {
...
}
}