老是在聊线程Thread,试试协程吧!

前言

本文主要基于Kotlin,以前写过一些Kotlin的文章,比较浅,有兴趣的小伙伴能够看上那么一看html

快速切换至Kotlin for Android模式android

充分理解Kotlin,快速上手写业务数据库

对于Java的小伙伴来讲,线程能够说是一个又爱又恨的家伙。线程能够带给咱们不阻碍主线程的后台操做,但随之而来的线程安全、线程消耗等问题又是咱们不得不处理的问题。编程

对于Java开发来讲,合理使用线程池能够帮咱们处理随意开启线程的消耗。此外RxJava库的出现,也帮助咱们更好的去线程进行切换。因此一直以来线程占据了个人平常开发...api

直到,我接触了协程...安全

正文

我们先来看一段Wiki上关于协程(Coroutine)的一些介绍:协程是计算机程序的一类组件,容许执行被挂起与被恢复。可是,到2003年,不少最流行的编程语言,包括C和它的后继,都未在语言内或其标准库中直接支持协程。在当今的主流编程环境里,线程是协程的合适的替代者...网络

可是!现在已经2019年了,协程真的没有用武之地么?!今天让咱们从Kotlin中感觉协程的有趣之处!并发

1、协程

开始实战以前,咱们聊一聊协程这么的概念。开启协程以前,咱们先说一说我们平常中的函数app

函数,在全部语言中都是层级调用,好比函数A调用函数B,函数B中又调用了函数C,函数C执行完毕返回,函数B执行完毕返回,最后是函数A执行完毕。异步

因此能够看出来函数的调用是经过栈实现的。

函数的调用老是一个入口,一次return,调用顺序是明确的。而协程的不一样之处就在于,执行过程当中函数内部是可中断的,也就是说中断以后,能够转而执行别的函数,在合适的时机再return回来继续执行没有执行完的内容。

而这种中断,叫作挂起。挂起咱们当前的函数,再某个合适的时机,才反过来继续执行~这里咱们再想一想回调:注册一个回调函数,在合适的时机执行这个回调。

  • 回调采用的是一种异步的形式
  • 而协程则是同步

是否是一时有点懵逼。不着急,咱往下看,往下更懵逼,哈哈~

2、Kotlin中的协程

经过Wiki上的介绍,咱们不难看出协程是一种标准。任何语言均可以选择去支持它。

这里是关于Kotlin中协程的文档:kotlinlang.org/docs/refere…

假设咱们想在android中的项目中使用协程该怎么办?很简单。

假设能够已经配好了Kotlin依赖

2.一、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.二、基本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.一、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.三、继续理解

接下来,我们来好好理解一下上面代码的含义。

首先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、协程的做用

3.一、拒绝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代码,直来直去,直接顺序逻辑写便可。

没错,这就是协程的做用之一。

  • 一、固然,不少小伙伴会说Java8引入的Future也能够完成相似的串行执行。(不过,话说回来是否是不少小伙伴没有升到Java8)...
  • 二、确定也有其余小伙伴说,我可使用Rx的方式,也能完成这种调用...

哈哈,彻底没错。由于你们都是为了解决一样的问题,可是协程还有其余好用的地方...

3.二、方便的线程切换

想一个咱们很常见的需求,子线程网络请求,数据回来后切到主线程更新UI。

runOnUiThread()、RxJava都能很方便的帮咱们切换线程。这里咱们看一下协程的方式:

GlobalScope.launch(Dispatchers.Main) {
    val result = withContext(Dispatchers.IO){
        // 网络请求,并return请求结果
        ... result
    }
    // 更新UI
    updateUI(result)
}
复制代码

很直来直去的逻辑,很直来直去的代码。可读性简直+100%。

withContext()能够方便的帮咱们在协程的上下文环境中切换线程,并返回执行结果。

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
}
复制代码

4、总结

这篇文章,主要是引出协程。协程不是一个新概念,不少语言都支持。

协程,引入了挂起的概念,让咱们的函数能够随意的暂停,而后在咱们原意的时候再执行。通知提供给了咱们同步写异步代码的能力...帮助咱们更高效的写代码,更直观的写代码。

尾声

关于协程,有不少不少的内容,能够聊。由于篇幅和时间的关系更多的细节,留给咱们接下来的文章吧。

我是一个应届生,最近和朋友们维护了一个公众号,内容是咱们在从应届生过渡到开发这一路所踩过的坑,以及咱们一步步学习的记录,若是感兴趣的朋友能够关注一下,一同加油~

我的公众号:咸鱼正翻身
相关文章
相关标签/搜索