本文主要基于Kotlin,以前写过一些Kotlin的文章,比较浅,有兴趣的小伙伴能够看上那么一看html
快速切换至Kotlin for Android模式android
对于Java的小伙伴来讲,线程能够说是一个又爱又恨的家伙。线程能够带给咱们不阻碍主线程的后台操做,但随之而来的线程安全、线程消耗等问题又是咱们不得不处理的问题。编程
对于Java开发来讲,合理使用线程池能够帮咱们处理随意开启线程的消耗。此外RxJava库的出现,也帮助咱们更好的去线程进行切换。因此一直以来线程占据了个人平常开发...api
直到,我接触了协程...安全
我们先来看一段Wiki上关于协程(Coroutine)的一些介绍:协程是计算机程序的一类组件,容许执行被挂起与被恢复。可是,到2003年,不少最流行的编程语言,包括C和它的后继,都未在语言内或其标准库中直接支持协程。在当今的主流编程环境里,线程是协程的合适的替代者...网络
可是!现在已经2019年了,协程真的没有用武之地么?!今天让咱们从Kotlin中感觉协程的有趣之处!并发
开始实战以前,咱们聊一聊协程这么的概念。开启协程以前,咱们先说一说我们平常中的函数:app
函数,在全部语言中都是层级调用,好比函数A调用函数B,函数B中又调用了函数C,函数C执行完毕返回,函数B执行完毕返回,最后是函数A执行完毕。异步
因此能够看出来函数的调用是经过栈实现的。
函数的调用老是一个入口,一次return,调用顺序是明确的。而协程的不一样之处就在于,执行过程当中函数内部是可中断的,也就是说中断以后,能够转而执行别的函数,在合适的时机再return回来继续执行没有执行完的内容。
而这种中断,叫作挂起。挂起咱们当前的函数,再某个合适的时机,才反过来继续执行~这里咱们再想一想回调:注册一个回调函数,在合适的时机执行这个回调。
是否是一时有点懵逼。不着急,咱往下看,往下更懵逼,哈哈~
经过Wiki上的介绍,咱们不难看出协程是一种标准。任何语言均可以选择去支持它。
这里是关于Kotlin中协程的文档:kotlinlang.org/docs/refere…
假设咱们想在android中的项目中使用协程该怎么办?很简单。
假设能够已经配好了Kotlin依赖
在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"
复制代码
先看一段官方的基础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%。
预警...这里将会引入大量的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) }
}
复制代码
接下来,我们来好好理解一下上面代码的含义。
首先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线程进行执行)。这样咱们就作到在子线程启动协程,完成咱们曾经线程的样子...
不少朋友,确定这里就产生疑问了。既然仍是用子线程作后台任务...那协程存在的意义有是什么呢?那接下来让我们走进协程的意义。
咱们平常开发时,常常会遇到这样的需求:好比一个发文流程中,咱们要先登陆;登陆成功后,咱们再进行发文;发文成功后咱们更新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代码,直来直去,直接顺序逻辑写便可。
没错,这就是协程的做用之一。
哈哈,彻底没错。由于你们都是为了解决一样的问题,可是协程还有其余好用的地方...
想一个咱们很常见的需求,子线程网络请求,数据回来后切到主线程更新UI。
runOnUiThread()
、RxJava都能很方便的帮咱们切换线程。这里咱们看一下协程的方式:
GlobalScope.launch(Dispatchers.Main) {
val result = withContext(Dispatchers.IO){
// 网络请求,并return请求结果
... result
}
// 更新UI
updateUI(result)
}
复制代码
很直来直去的逻辑,很直来直去的代码。可读性简直+100%。
withContext()
能够方便的帮咱们在协程的上下文环境中切换线程,并返回执行结果。
咱们再来看一段官方代码:
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
}
复制代码
这篇文章,主要是引出协程。协程不是一个新概念,不少语言都支持。
协程,引入了挂起的概念,让咱们的函数能够随意的暂停,而后在咱们原意的时候再执行。通知提供给了咱们同步写异步代码的能力...帮助咱们更高效的写代码,更直观的写代码。
关于协程,有不少不少的内容,能够聊。由于篇幅和时间的关系更多的细节,留给咱们接下来的文章吧。