Kotlin实战指南十三:协程

转载请标明出处:https://blog.csdn.net/zhaoyanjun6/article/details/95626034
本文出自【赵彦军的博客】


前言-协程介绍

协程又称微线程,从名字可以看出,协程的粒度比线程更小,并且是用户管理和控制的,多个协程可以运行在一个线程上面。那么协程出现的背景又是什么呢,先来看一下目前线程中影响性能的特性:

  • 使用锁机制
  • 线程间的上下文切换
  • 线程运行和阻塞状态的切换

以上任意一点都是很消耗cpu性能的。相对来说协程是由程序自身控制,没有线程切换的开销,且不需要锁机制,因为在同一个线程中运行,不存在同时写变量冲突,在协程中操作共享资源不加锁,只需要判断状态就行了,所以执行效率比线程高的多。

But , But , But , But , But , But , But , But , But , But , But .......

在 kotlin 语言环境下,协程 仅仅是一个线程框架 , 并没有什么高深的东西,这一点会把很多初学者搞晕。

主流语言对协程的支持

  • Lua语言

Lua从5.0版本开始使用协程,通过扩展库coroutine来实现。

  • Python语言

python可以通过 yield/send 的方式实现协程。在python 3.5以后,async/await 成为了更好的替代方案。

  • Go语言

Go语言对协程的实现非常强大而简洁,可以轻松创建成百上千个协程并发执行。

  • Java语言

如上文所说,Java语言并没有对协程的原生支持,但是某些开源框架模拟出了协程的功能,有兴趣的小伙伴可以看一看Kilim框架的源码:https://github.com/kilim/kilim

  • C/C++

c/c++需要自己借助ucontext、setjmp、longjmp库实现,微信开源了c/c++的协程库libco。

Android 项目引用

Kotlin 协程库的GitHub地址:https://github.com/Kotlin/kotlinx.coroutines/tree/master/ui/kotlinx-coroutines-android

Gradle 引用

implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.2.1"

创建一个协程

class MainActivity : AppCompatActivity() {

    var tv1: TextView? = null

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        tv1 = findViewById(R.id.tv1)
        
        //在主线程启动一个协程
        GlobalScope.launch(Dispatchers.Main) {
            // launch coroutine in the main thread
            for (i in 10 downTo 1) { // countdown from 10 to 1
                tv1?.text = "Countdown $i ..." // update text
                delay(1000) // wait a second
            }
            tv1?.text = "Done!"
        }
    }
}

如果你仔细观察,你会发现,耗时操作和更新UI 放在一起执行了,纳尼?
你会有这样的疑问,怎么没有线程切换,这难道不会卡顿吗?
答案是不会的,这就是协程的牛逼之处。

还有一点需要注意,上面的代码中,我们使用 delay(1000) 来做延时操作,delay 是一个特殊的函数,这里暂且称之为挂起函数,它不会阻塞线程,但是会挂起协程,而且它只能在协程中使用。

再延伸一点,我们能否用 Thread.sleep(1000) 来代替 delay(1000) , 答案是不能的。我们的协程是在主线程的基础上创建的,本质上是主线程的小逻辑单元,用 Thread.sleep(1000) 会直接卡死 UI 主线程。

取消协程工作

java开发Android应用时,我们用子线程执行耗时操作,当然我们也会中断子线程来达到取消耗时操作的目的。
那么我们在协程中执行耗时操作的时候,改怎么取消呢?

GlobalScope.launch 的返回值是 Job 对象,用 job.cancel() 来取消协程。例子如下:

class MainActivity : AppCompatActivity() {

    var tv1: TextView? = null
    var mCancelButton: Button? = null

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        tv1 = findViewById(R.id.tv1)
        mCancelButton = findViewById(R.id.cancel)

        //在主线程启动一个协程
        var job = GlobalScope.launch(Dispatchers.Main) {
            // launch coroutine in the main thread
            for (i in 10 downTo 1) { // countdown from 10 to 1
                tv1?.text = "Countdown $i ..." // update text
                delay(1000) // wait a second
            }
            tv1?.text = "Done!"
        }

        mCancelButton?.setOnClickListener {
            job.cancel() //取消协程工作
            mCancelButton?.text = "已经取消了"
        }
    }
}

launch 参数详解

在上文中,我们已经学会了使用 GlobalScope.launch 创建一个协程,下面我们来看看创建协程所需要的参数,launch 的参数有三个,依次为协程上下文协程启动模式协程体

public fun CoroutineScope.launch(
    context: CoroutineContext = EmptyCoroutineContext,  //上下文`
    start: CoroutineStart = CoroutineStart.DEFAULT,   //启动模式
    block: suspend CoroutineScope.() -> Unit   //协程体
): Job

启动模式不是一个很复杂的概念,不过我们暂且不管,默认直接允许调度执行。

上下文可以有很多作用,包括携带参数拦截协程执行等等,多数情况下我们不需要自己去实现上下文,只需要使用现成的就好。上下文有一个重要的作用就是线程切换,Dispatchers.Main就是一个官方提供的上下文,它可以确保 launch 启动的协程体运行在UI线程当中(除非你自己在 launch 的协程体内部进行线程切换、或者启动运行在其他有线程切换能力的上下文的协程)。

协程体就是我们具体执行的代码

线程调度器 Dispatchers

上面我们创建协程的时候,用的是:

GlobalScope.launch(Dispatchers.Main) {
   //do some things
}

为了指定coroutines在什么线程运行,kotlin提供了四种Dispatchers:

Dispatchers用途使用场景
Dispatchers.Main主线程,和UI交互,执行轻量任务1.call suspend functions。2. call UI functions。 3. Update LiveData
Dispatchers.IO用于网络请求和文件访问1. Database。 2.Reading/writing files。3. Networking
Dispatchers.DefaultCPU密集型任务1. Sorting a list。 2.Parsing JSON。 3.DiffUtils
Dispatchers.Unconfined不限制任何制定线程高级调度器,不应该在常规代码里使用

withContext

上面的部分,我们介绍了调度器 Dispatchers , 那么具体是怎么切换线程的,就是用 withContext 函数。

public suspend fun <T> withContext(
    context: CoroutineContext,
    block: suspend CoroutineScope.() -> T
):

withContextsuspend 修饰,说明 suspend 是一个挂起函数。

withContext(Dispatchers.IO) 定义一段代码块,这个代码块将在调度器 Dispatchers.IO中运行,方法块中的任何代码总是会运行在 IO调度器中。

举个例子:

// Dispatchers.Main
suspend fun fetchDocs() {
    // Dispatchers.Main
    val result = get("developer.android.com")
    // Dispatchers.Main
    show(result)
}

// Dispatchers.Main
suspend fun get(url: String) =
    // Dispatchers.IO
    withContext(Dispatchers.IO) {
        // Dispatchers.IO
        /* perform blocking network IO here */
    }
    // Dispatchers.Main
}

通过协程,你可以细粒度的控制线程调度,因为 withContext 让你可以控制任意一行代码运行在什么线程上,而不用引入回调来获取结果。你可将其应用在很小的函数中,例如数据库操作和网络请求。所以,比较好的做法是,使用 withContext确保每个函数在任意调度器上执行都是安全的,包括 Main,这样调用者在调用函数时就不需要考虑应该运行在什么线程上。

withContext 的性能

对于提供主线程安全性,withContext 与回调或 RxJava一样快。在某些情况下,甚至可以使用协程上下文 withContext 来优化回调。如果一个函数将对数据库进行10次调用,那么您可以告诉 Kotlin在外部的 withContext中调用一次切换。尽管数据库会重复调用 withContext,但是他它将在同一个调度器下,寻找最快路径。此外,Dispatchers.DefaultDispatchers.IO 之间的协程切换已经过优化,以尽可能避免线程切换。

综合演练

下面我们来模拟一个真实的网络请求

class MainActivity : AppCompatActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        //在子线程启动一个协程
        GlobalScope.launch(Dispatchers.IO) {

            //发起一个网络请求
            var result = HttpUtil.get("https://www.baidu.com")

            Log.e("zhaoyanjun:22", "${Thread.currentThread().name}")

            withContext(Dispatchers.Main) {
                //网络请求成功以后,到主线程更新UI
                Log.e("zhaoyanjun:33", "${Thread.currentThread().name}")
            }

            //再次回到子线程的协程
            Log.e("zhaoyanjun:44", "${Thread.currentThread().name}")
        }
    }
}

日志打印结果:

E/zhaoyanjun:22: DefaultDispatcher-worker-2 
E/zhaoyanjun:33: main
E/zhaoyanjun:44: DefaultDispatcher-worker-2

协程到底是什么

好,坚持读到这里的朋友们,你们一定是异步代码的“受害者”,你们肯定遇到过“回调地狱”,它让你的代码可读性急剧降低;也写过大量复杂的异步逻辑处理、异常处理,这让你的代码重复逻辑增加;因为回调的存在,还得经常处理线程切换,这似乎并不是一件难事,但随着代码体量的增加,它会让你抓狂,线上上报的异常因线程使用不当导致的可不在少数。

而协程可以帮你优雅的处理掉这些。

简单来说就是,协程是一种非抢占式或者说协作式的计算机程序并发调度的实现,程序可以主动挂起或者恢复执行。这里还是需要有点儿操作系统的知识的,我们在 Java 虚拟机上所认识到的线程大多数的实现是映射到内核的线程的,也就是说线程当中的代码逻辑在线程抢到 CPU 的时间片的时候才可以执行,否则就得歇着,当然这对于我们开发者来说是透明的;而经常听到所谓的协程更轻量的意思是,协程并不会映射成内核线程或者其他这么重的资源,它的调度在用户态就可以搞定,任务之间的调度并非抢占式,而是协作式的。

如果大家熟悉 Java 虚拟机的话,就想象一下 Thread 这个类到底是什么吧,为什么它的 run 方法会运行在另一个线程当中呢?谁负责执行这段代码的呢?显然,咋一看,Thread 其实是一个对象而已,run 方法里面包含了要执行的代码——仅此而已。协程也是如此,如果你只是看标准库的 API,那么就太抽象了,但我们开篇交代了,学习协程不要上来去接触标准库,kotlinx.coroutines 框架才是我们用户应该关心的,而这个框架里面对应于 Thread 的概念就是 Job 了,大家可以看下它的定义:

public interface Job : CoroutineContext.Element {
    ...
    public val isActive: Boolean
    public val isCompleted: Boolean
    public val isCancelled: Boolean

    public fun start(): Boolean
    public fun cancel(cause: CancellationException? = null)
    public suspend fun join()
    ...
}

我们再来看看 Thread 的定义:

public class Thread implements Runnable {
    ...    
    public final native boolean isAlive();
    public synchronized void start() { ... }
    @Deprecated
    public final void stop() { ... }
    public final void join() throws InterruptedException  { ... }
    ...
}

这里我们非常贴心的省略了一些注释和不太相关的接口。我们发现,Thread 与 Job 基本上功能一致,它们都承载了一段代码逻辑(前者通过 run 方法,后者通过构造协程用到的 Lambda 或者函数),也都包含了这段代码的运行状态。
而真正调度时二者才有了本质的差异,具体怎么调度,我们只需要知道调度结果就能很好的使用它们了。

参考资料

Kotlin中文社区 https://www.jianshu.com/p/086a0d681f29

高杰:在Android中使用协程 https://juejin.im/post/5cea3ee0f265da1bca51b841


个人微信号:zhaoyanjun125 , 欢迎关注

  • 4
    点赞
  • 14
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值