协程 Coroutines
,指各任务协作运行;
线程是操作系统层面的,由操作系统调度执行,我们可以开启一个线程,但无法知道线程什么时候执行,什么时候执行完,因此我们通常使用回调的形式在线程执行完之后接受执行的结果,线程的运行是抢占式的,后起的 B 线程可能抢占先起的 A 线程的资源,A 线程会被阻塞,从而造成资源的浪费。
协程是应用层面的,它由虚拟机进行调度,我们可以随意开启和终止协程的运行,协程是非抢占氏的,如果当前协程在运行,除非当前运行的协程主动 退让,挂起,否则其他协程不会抢占运行机会,由于各任务写作运行,就避免了创建大量的线程。
协程本身并不具备线程切换的功能,耗时操作等仍旧需要我们手动切换到子线程执行,但是协程的 API 设计使得我们可以像编写同步代码一样编写异步代码,避免使用回调,逻辑也更清晰。
本文中的代码没有真正的进行操作,使用输出 log
来代替
推荐阅读
使用回调进行异步
使用回调接口获取异步结果,是在 Java
中常用的方法,在主线程进行 UI
操作,切换到子线程进行耗时操作,操作的结果返回到主线程更新 UI
。
1 | // 线程池 |
因为我们只有一层回调,加上 Kotlin
对 Lambda
表达式的支持,看起来代码还不是那么难看,这也是在 Java
中常用的方法,相对好理解一些,看一下输出结果:
1 | E/Coroutine: main 开始先来个UI操作 |
使用协程实现
协程借助 suspend
关键字实现,使用 suspend
关键字描述的函数,表示此函数可以被挂起。
介绍几个概念
CoroutineContext
,协程上下文,很多框架都会有一个上下文对象,支持 +
操作符,因此它可以是多个上下文的累加结果,Kotlin
中实现了一个空的实现 EmptyCoroutineContext
,用来占位。
Continuation
,协程中的任务执行后的操作,它可以返回任务的结果或者异常。
同样使用协程来实现一个和上面类似的功能:
1 | fun testCoroutine() { |
输出结果:
1 | E/Coroutine: main 开始先来个UI操作 |
我们看到,最后结果的 UI
操作,还是在子线程跑的,这个是不允许的,但是从代码上面,最后打印结果的代码明明是在主线程,其实 var resule = xx
之后的代码就如同回调中的代码,它的运行环境取决于 Continuation
的 resume()
方法执行的环境,在这段协程的代码中,最关键的一句是
1 | val result = doLotsOfThingsCanSuspend("test") |
它替代了原先的回调方法,也实现了类似原来回调的功能,当运行到这边时,函数会被挂起,等结果返回之后才会赋值并且进行后面的操作,避免了层层回调嵌套,另外虽然最后拿到结果的代码看起来是写在主线程,其实最后运行在了子线程,因此我们可以如下,修改代码,保证最后的结果被传递回主线程。
1 | // 一个耗时的方法,可能被挂起 |
但是对于这个操作,借助 Continuation
和 CoroutineContext
可以有更优雅的实现。
将结果返回切换到主线程
写一个 Continuation
的包装类 UIContinuation
,这个很好理解,一个典型的包装者模式,他包装其他的 Continuation
在调用 resume()
方法时将结果返回主线程。
1 | class UIContinuation<in T>(val delegate: Continuation<T>) : Continuation<T> { |
重写 CoroutineContext
自动完成 Continuation
的包装,需要一个接收外界设置的 CoroutineContext
的 Continuation
,他其实什么也不需要做。
1 | class ContextContinuation(override val context: CoroutineContext = EmptyCoroutineContext) : Continuation<Unit> { |
重写 CoroutineContext
使用插值器的方式包装,这里使用了 ContinuationInterceptor
,他也是 CoroutineContext
的子类,类似 okHttp
的 interceptor
,他的作用就是,拦截旧的 Continuation
如果需要的话生成新的 Continuation
.
1 | class AsyncCoroutineContext : |
此时,调用如下方法启动协程,配置可以将结果返回主线程的上下文,当结果返回时会自动返回主线程。
1 | fun callCoroutine(block: suspend () -> Unit) { |
使用 CoroutineContext 传参
首先,CoroutineContext
有一个不错的特性,就是重载了 +
操作符,因为我们可以对 CoroutineContext
进行加法操作返回新的 CoroutineContext
为了方便使用,我们将基础的方法进行提取,他们实现一个通用的功能,即切换后台线程执行耗时操作,然后在主线程接收结果更改UI。
1 | // 开始协程,可以从外界接受上下文变量 |
借助上面的抽象出来的两个方法可以开启协程,执行任意耗时操作,并将结果返回到主线程,接下来使用 ConroutineContext
进行参数的传递,从上面我们发现 block
函数是一个无参的函数,而各种耗时操作参数不一,怎么兼容这个问题,就需要使用 ConroutineContext
,他是一个上下文,是整个运行环境,借助它我们可以传递参数,定义一个携带参数的上下文,它仍旧是 AbstractCoroutineContextElement
的子类,并标记一个 Key
1 | // 携带上下文的 context |
从上下文中获取参数,重点在于 this[ParamContext.Key]
,也可以写作 this[ParamContext]
,因为那个 Key
是一个伴生对象,能用下标访问是因为 CoroutineContext
重载了 []
操作符,从而可以取出之前传递的 ParamContext
1 | fun testCoroutine() { |
Sequence
官方的一个例子
1 | val fibonacci = buildSequence { |
每次对 Sequence
迭代遍历下一个时,会执行协程到 yield
的位置返回值,并挂起,等待下一次迭代,这样就构造了一个懒序列,只有在需要的时候才会计算生成,而不是一次生成大量数据,除了第一次,他的每次被调起,都是从这次 yield
到下次 yield
总结
使用 suspendCoroutine
构造一个可挂起的函数,在内部执行操作,通过 Continuation
的 resume()
方法将结果或者异常返回,你可以决定返回之前是不是要进行自己的处理,比如发到主线程。
Continuation
是一个接口,他是一个结果的接受处理者,这也是 Continuation
的含义,他将在任务完成之后被调用。
CoroutineContext
是一个上下文,表示一个运行时环境,它可以做很多事,比如传递参数等。
ContinuationInterceptor
是 Continuation
的插值器,提供一个更改原来的 Continuation
的机会,用来创建返回新的 Continuation
。
从代码层面上面,可以跟回调的方法稍作比较,下面代码中,其实 var result = xxx
这里就相当于一个回调,但是并不是回调的形式,在这里执行 doLotsOfThingsCanSuspend()
时,函数会被挂起,等待结果返回,这不是一个阻塞的过程,异步线程的切换在挂起函数内进行,因此我们可以像写同步代码一样,书写异步代码,结果返回之后,后面的代码其实相当于回调里面的代码,此时才会继续执行,和回调一样,它的运行环境取决于挂起函数中调用 resume()
方法时的环境。
1 | callCoroutine { |
Kotlinx 官方协程扩展框架
添加依赖
1 | compile 'org.jetbrains.kotlinx:kotlinx-coroutines-core:0.18' |
借助 launch()
函数来开启一个协程,CommonPool
是内置的一个上下文,他将会在子线程执行任务。
1 | fun test1() { |
输出结果为
1 | E/tag: main 开始协程 |
结果就好像我们在子线程执行了一个任务,他并没有干预外面的代码运行,也没有获得预期的值,我们希望后面的任务在子线程的代码计算完成之后在执行,需要使用 join()
函数。
launch()
函数返回一个 Job
对象,同时 join()
函数需要在一个可挂起的函数内执行,使用 runBlocking()
函数使用 EmptyCoroutineContext
可以在当前位置开启协程;
这样就得到了预期的结果,同样我们可以调用 job.cancel()
来结束这个任务
1 | fun test2() { |
输出结果为
1 | E/tag: main 开始协程 |
获取返回值,上面我们使用了变量的形式来存储计算的值,当我们需要一个任务的返回值时,最好还是让任务能够返回执行完的结果,使用 async()
函数可以返回一个 Deferred
对象,从里面可以取到执行结果
1 | launch(CommonPool) { |
输出结果
1 | E/tag: ForkJoinPool.commonPool-worker-1 result,100 |
在子线程计算,在主线程更新,UI
是一个协程上下文,他使用 Handler
将结果分发到主线程。
1 | launch(UI) { |