前言
本文主要基于Kotlin,之前写过一些Kotlin的文章,比较浅,有兴趣的小伙伴可以看上那么一看
对于Java的小伙伴来说,线程可以说是一个又爱又恨的家伙。线程可以带给我们不阻碍主线程的后台操作,但随之而来的线程安全、线程消耗等问题又是我们不得不处理的问题。
对于Java开发来说,合理使用线程池可以帮我们处理随意开启线程的消耗。此外RxJava库的出现,也帮助我们更好的去线程进行切换。所以一直以来线程占据了我的日常开发...
直到,我接触了协程...
正文
咱们先来看一段Wiki上关于协程(Coroutine)的一些介绍:协程是计算机程序的一类组件,允许执行被挂起与被恢复。但是,到2003年,很多最流行的编程语言,包括C和它的后继,都未在语言内或其标准库中直接支持协程。在当今的主流编程环境里,线程是协程的合适的替代者...
但是!如今已经2019年了,协程真的没有用武之地么?!今天让我们从Kotlin中感受协程的有趣之处!
一、协程
开始实战之前,我们聊一聊协程这么的概念。开启协程之前,我们先说一说咱们日常中的函数:
函数,在所有语言中都是层级调用,比如函数A调用函数B,函数B中又调用了函数C,函数C执行完毕返回,函数B执行完毕返回,最后是函数A执行完毕。
所以可以看出来函数的调用是通过栈实现的。
函数的调用总是一个入口,一次return,调用顺序是明确的。而协程的不同之处就在于,执行过程中函数内部是可中断的,也就是说中断之后,可以转而执行别的函数,在合适的时机再return回来继续执行没有执行完的内容。
而这种中断,叫做挂起。挂起我们当前的函数,再某个合适的时机,才反过来继续执行~这里我们再想想回调:注册一个回调函数,在合适的时机执行这个回调。
- 回调采用的是一种异步的形式
- 而协程则是同步
是不是一时有点懵逼。不着急,咱往下看,往下更懵逼,哈哈~
二、Kotlin中的协程
通过Wiki上的介绍,我们不难看出协程是一种标准。任何语言都可以选择去支持它。
这里是关于Kotlin中协程的文档:https://kotlinlang.org/docs/reference/coroutines-overview.html
假设我们想在android中的项目中使用协程该怎么办?很简单。
假设可以已经配好了Kotlin依赖
2.1、gradle引入
在Android中协程的引入非常的简单,只需要在gradle中:
apply plugin: 'kotlin-android-extensions'
然后依赖中添加:
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:1.1.0"
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.1.0"
2.2、基本demo
先看一段官方的基础demo:
// 启动一个协程
GlobalScope.launch(Dispatchers.Main) {
// 执行一个延迟10秒的函数
delay(10000L)
println("World!-${Thread.currentThread().name}")
}
println("Hello-${Thread.currentThread().name}-")
这段代码执行结果应该大家都能猜到:Hello-main-World!-main
。大家有没有注意到,这俩个输出,全部打印了main线程。
这段代码在主线程执行,并且延迟了10秒钟,而且也没有出现ANR!
当然,这里有小伙伴会说,我可以通过Handler进行
postDelay()
也能做到这种效果。没错,我们的postDelay()
,是一种回调的解决方案。而我们开头提到过,协程使用同步的方式去解决这类问题。
所以,协程中的delay()
也是通过队列实现的。但是!它用同步的形式屏弃掉了回调,让我们的代码可读性+100%。
2.2.1、delay()的实现
预警...这里将会引入大量的Kotlin中的协程api。为了避免阅读不适。这一小节建议直接跳过
跳过总结:
Kotlin为我们提供了一些api,帮我们能够摆脱CallBack,本质也是通过封装CallBack的形式,实现同步化异步代码。
public suspend fun delay(timeMillis: Long) {
if (timeMillis <= 0) return // don't delay
// 很明显可以看出,实现仍然是用CallBack的形式
return suspendCancellableCoroutine sc@ { cont: CancellableContinuation<Unit> ->
cont.context.delay.scheduleResumeAfterDelay(timeMillis, cont)
}
}
/** Returns [Delay] implementation of the given context */
internal val CoroutineContext.delay: Delay get() = get(ContinuationInterceptor) as? Delay ?: DefaultDelay
internal actual val DefaultDelay: Delay = DefaultExecutor
delay()
使用suspendCancellableCoroutine()
挂起协程,一般情况下控制协程恢复的关键在DefaultExecutor.scheduleResumeAfterDelay()
中,中实现是schedule(DelayedResumeTask(timeMillis, continuation))
,关键逻辑是将DelayedResumeTask
放到DefaultExecutor
的队列最后,在延迟的时间到达就会执行DelayedResumeTask
,那么该 task 里面的实现是什么:
override fun run() {
// 直接在调用者线程恢复协程
with(cont) { resumeUndispatched(Unit) }
}
2.3、继续理解
接下来,咱们来好好理解一下上面代码的含义。
首先delay()
被称之为挂起函数,这种函数在协程的作用域中,可以被挂起,挂起后不阻塞当前线程中协程作用域以外的代码执行。并且协程会在合适的时机,恢复挂起继续执行协程作用域中后续的代码。
而上述代码中的GlobalScope.launch(Dispatchers.Main) {}
,就是在主线程创建一个全局的协程作用域。而我们的delay(10000)
是一个挂起函数,执行到它的时候,协程会挂起此函数。让出CPU,此时我们协程作用域之外的println("Hello-${Thread.currentThread().name}-")
就有机会执行了。
当合适的时机到来,也就是10000毫秒过后。协程会恢复挂起函数,继续执行后续的代码。
思考
看到这,我猜肯定有小伙伴,内心卧槽了一声:“这不完全不需要线程了?以后阻塞操作,直接写在挂起函数了?”。这是完全错误的想法!协程提供的是同步化异步代码的能力。协程是在用户态帮我们封装了对应的异步api。而不是真正提供了异步的能力。所以如果我们在主线程的协程中进行IO操作,一样会阻塞住主线程。
GlobalScope.launch(Dispatchers.Main) {
...网络请求/...大量数据的数据库操作
}
一样会抛出NetworkOnMainThread
/一样会阻塞主线程。因为上述代码,本质还是在主线程执行。所以假设我们在协程中运行阻塞当前线程的代码(比如IO操作),仍然会阻塞住当前的线程。也就是有可能出现我们常见的ANR。
因此,在这种场景下,我们需要这么调用:
GlobalScope.launch(Dispatchers.IO) {
...网络请求/...大量数据的数据库操作
}
我们在启动一个协程的时候,改了一个新的协程上下文(这个上下文会将协程切换到IO线程进行执行)。这样我们就做到在子线程启动协程,完成我们曾经线程的样子...
思考
很多朋友,肯定这里就产生疑问了。既然还是用子线程做后台任务...那协程存在的意义有是什么呢?那接下来让咱们走进协程的意义。
三、协程的作用
3.1、拒绝CallBack
我们日常开发时,经常会遇到这样的需求:比如一个发文流程中,我们要先登录;登录成功后,我们再进行发文;发文成功后我们更新UI。
来段伪码,简单实现一下这样的需求:
// 登录的伪码。传递一个lambda,也就是一个CallBack
fun login(cb: (User) -> Unit) { ... }
// 发文的伪码
fun postContent(user: User, content: String, cb: (Result) -> Unit) { ... }
// 更新UI
fun updateUI(result: Result) { ... }
fun ugcPost(content: String) {
login { user ->
postContent(user, content) { result ->
updateUI(result)
}
}
}
这种需求下,我们通常会由俩个CallBack完成这种串行的需求。不知道大家日常写这种代码的时候,有没有思考过,为什么串行的逻辑,要用CallBack的形式(异步)完成?
可能大家会说:这些需求要用线程去进行后台执行,只能通过CallBack拿到结果。
那么问题又来了,为什么用线程做后台逻辑时,我们就必须要用CallBack呢?毕竟从我们的思维逻辑上来说,这些需求就是串行,理论上顺序执行代码就ok了。所以协程的作用就出现了...
这种通过异步形式的逻辑,在协程的辅助下就可变成同步执行:
// 挂起函数,不需要任何CallBack,我们CallBack的内容,只需要当做返回值return即可
suspend fun login(): User { ... }
suspend fun postContent(user: User, content: String): Result { ... }
fun updateUI(result: Result) { ... }
fun ugcPost(content: String) {
GlobalScope.launch {
val user = login()
val result = postContent(user, content)
updateUI(result)
}
}
这样我们就完成了原本需要层层嵌套的CallBack代码,直来直去,直接顺序逻辑写即可。
没错,这就是协程的作用之一。
- 1、当然,很多小伙伴会说Java8引入的Future也可以完成类似的串行执行。(不过,话说回来是不是很多小伙伴没有升到Java8)...
- 2、肯定也有其他小伙伴说,我可以使用Rx的方式,也能完成这种调用...
哈哈,完全没错。因为大家都是为了解决同样的问题,但是协程还有其他好用的地方...
3.2、方便的线程切换
想一个我们很常见的需求,子线程网络请求,数据回来后切到主线程更新UI。
runOnUiThread()
、RxJava都能很方便的帮我们切换线程。这里我们看一下协程的方式:
GlobalScope.launch(Dispatchers.Main) {
val result = withContext(Dispatchers.IO){
// 网络请求,并return请求结果
... result
}
// 更新UI
updateUI(result)
}
很直来直去的逻辑,很直来直去的代码。可读性简直+100%。
withContext()
可以方便的帮我们在协程的上下文环境中切换线程,并返回执行结果。
3.3、方便的并发
我们再来看一段官方代码:
import kotlinx.coroutines.*
import kotlin.system.*
fun main() = runBlocking<Unit> {
val time = measureTimeMillis {
val one = doSomethingUsefulOne()
val two = doSomethingUsefulTwo()
println("The answer is ${one + two}")
}
println("Completed in $time ms")
}
suspend fun doSomethingUsefulOne(): Int {
delay(1000L) // 假设我们在这里做了些有用的事
return 13
}
suspend fun doSomethingUsefulTwo(): Int {
delay(1000L) // 假设我们在这里也做了一些有用的事
return 29
}
输出结果如下:
The answer is 42
Completed in 2017 ms
假设我们耗时计算操作,没有任何依赖关系。因此最佳的方案,就是让它们俩并行执行。如何让doSomethingUsefulOne()
、doSomethingUsefulTwo()
同时执行呢?
答案是:async + await
fun main() = runBlocking<Unit> {
val time = measureTimeMillis {
val one = async { doSomethingUsefulOne() }
val two = async { doSomethingUsefulTwo() }
println("The answer is ${one.await() + two.await()}")
}
println("Completed in $time ms")
}
suspend fun doSomethingUsefulOne(): Int {
delay(1000L) // 假设我们在这里做了些有用的事
return 13
}
suspend fun doSomethingUsefulTwo(): Int {
delay(1000L) // 假设我们在这里也做了些有用的事
return 29
}
四、总结
这篇文章,主要是引出协程。协程不是一个新概念,很多语言都支持。
协程,引入了挂起的概念,让我们的函数可以随意的暂停,然后在我们原意的时候再执行。通知提供给了我们同步写异步代码的能力...帮助我们更高效的写代码,更直观的写代码。
尾声
关于协程,有很多很多的内容,可以聊。因为篇幅和时间的关系更多的细节,留给我们接下来的文章吧。
网友评论