目录:
协程就像非常轻量级的线程。线程是由系统调度的,线程切换或线程阻塞的开销都比较大。而协程依赖于线程,但是协程挂起时不需要阻塞线程,几乎是无代价的,协程是由开发者控制的。
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)
}
}
}
这种多层嵌套的方式比较复杂,而且不方便处理异常情况。
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 { ... }
fun createPostAsync(token: Token, item: Item): CompletableFuture { ... }
fun processPost(post: Post) { ... }
fun postItem(item: Item) {
requestTokenAsync()
.thenCompose { token -> createPostAsync(token, item) }
.thenAccept { post -> processPost(post) }
.exceptionally { e ->
e.printStackTrace()
null
}
}
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线程。
我们写的有两个方法是挂起的函数(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 ->
cont.context.delay.scheduleResumeAfterDelay(timeMillis, cont)
}
}
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 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)可以是多个上下文的组合。组合的上下文需要是不同的类型。所以,你需要做两件事情:
操作符号 + 用于组合上下文。如果两种不同类型的上下文相组合,会生成一个组合的上下文(CombinedContext),这个新的上下文会同时拥有被组合上下文的特性。因为:get() = Dispatchers.Main + job
,所以launch方法实际上是在Dispatchers.Main,也就是在UI线程中执行的。
CoroutineScope 可以理解为一个协程,里面有一个协程的上下文:CoroutineContext,这个协程上下文包含很多该协程的信息,比如:Job, ContinuationInterceptor, CoroutineName 和CoroutineId。在CoroutineContext中,是用map来存这些信息的, map的键是这些类的伴生对象,值是这些类的一个实例,你可以这样子取得context的信息:
val job = context[Job]
val continuationInterceptor = context[ContinuationInterceptor]
CoroutineDispatcher,协程调度器,决定协程所在的线程或线程池。它可以指定协程运行于特定的一个线程、一个线程池或者不指定任何线程(这样协程就会运行于当前线程)。coroutines-core中 CoroutineDispatcher 有四种标准实现Dispatchers.Default、Dispatchers. IO,Dispatchers.Main 和 Dispatchers.Unconfined,Unconfined 就是不指定线程。
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
创建一个新的协程来阻塞当前线程,直到 runBlocking 代码块执行完成。通常它不会用于协程中,因为在协程中写一个阻塞的代码块实在太别扭,可以通过挂起操作取代。它通常作为一个适配器,将 main 线程转换成一个 main 协程,我们也就持有了一个 main 协程的 coroutineContext 上下文对象,就可以随心所欲用(this)使用 coroutineContext 的扩展方法,随心所欲使用 suspend 方法 ( suspend 方法只能用于 suspend 方法和协程中)。所以 runBlocking 一般用在 test 函数和 main 函数中。
withContext 不会创建一个新的协程,在指定的协程上运行代码块,并挂起该协程直到代码块运行完成。通常是用于切换协程的上下文。
例如:
// 使用 withContext 切换协程,上面的例子就是先在 IO 线程里执行,然后切换到主线程。
GlobalScope.launch(Dispatchers.IO) {
...
withContext(Dispatchers.Main) {
...
}
}