阅读 1615

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

前言

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

快速切换至Kotlin for Android模式

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

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

对于Java开发来说,合理使用线程池可以帮我们处理随意开启线程的消耗。此外RxJava库的出现,也帮助我们更好的去线程进行切换。所以一直以来线程占据了我的日常开发...

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

正文

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

但是!如今已经2019年了,协程真的没有用武之地么?!今天让我们从Kotlin中感受协程的有趣之处!

一、协程

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

函数,在所有语言中都是层级调用,比如函数A调用函数B,函数B中又调用了函数C,函数C执行完毕返回,函数B执行完毕返回,最后是函数A执行完毕。

所以可以看出来函数的调用是通过栈实现的。

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

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

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

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

二、Kotlin中的协程

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

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

假设我们想在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
}
复制代码

四、总结

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

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

尾声

关于协程,有很多很多的内容,可以聊。因为篇幅和时间的关系更多的细节,留给我们接下来的文章吧。

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

个人公众号:咸鱼正翻身

关注下面的标签,发现更多相似文章
评论