精选文章 Kotlin学习之协程

Kotlin学习之协程

作者:jingerlovexiaojie 时间: 2021-02-05 09:43:12
jingerlovexiaojie 2021-02-05 09:43:12
【摘要】协程是什么? 
一个线程框架,其实就是一套由Kotlin官方提供的线程API。就像 Java 中的 Executor 和 Android 中的 AsyncTask,Kotlin 中的协程也有对 Thread API 的封装,让我们可以在写代码时,不用关注多线程就能够很方便地写出并发操作。 
协程好在哪里? 
方便。可以在同一个代码块里进行多次的线程切换。 
协程最常用的功能是并发,而并发的典...

协程是什么?

一个线程框架,其实就是一套由Kotlin官方提供的线程API。就像 Java 中的 Executor 和 Android 中的 AsyncTask,Kotlin 中的协程也有对 Thread API 的封装,让我们可以在写代码时,不用关注多线程就能够很方便地写出并发操作。

协程好在哪里?

方便。可以在同一个代码块里进行多次的线程切换。

协程最常用的功能是并发,而并发的典型场景就是多线程。可以使用 Dispatchers.IO 参数把任务切到 IO 线程执行:

coroutineScope.launch(Dispatchers.IO) { ...
}

也可以使用 Dispatchers.Main 参数切换到主线程:

coroutineScope.launch(Dispatchers.Main) { ...
}

协程最大的好处是可以把运行在不同线程的代码写在同一个代码块里面。

coroutineScope.launch(Dispatchers.Main) {   // 在主线程开启协程 val user = api.getUser() // IO 线程执行网络请求 nameTv.text = user.name  // 主线程更新 UI
}

对于回调式的写法,如果并发场景再复杂一些,代码的嵌套可能会更多,这样的话维护起来就非常麻烦。但如果你使用了 Kotlin 协程,多层网络请求只需要这么写:

coroutineScope.launch(Dispatchers.Main) { // 开始协程:主线程 val token = api.getToken() // 网络请求:IO 线程 val user = api.getUser(token) // 网络请求:IO 线程 nameTv.text = user.name // 更新 UI:主线程
}

如果遇到的场景是多个网络请求需要等待所有请求结束之后再对 UI 进行更新。

coroutineScope.launch(Dispatchers.Main) { // 👇  async 函数之后再讲 val avatar = async { api.getAvatar(user) } // 获取用户头像 val logo = async { api.getCompanyLogo(user) } // 获取用户所在公司的 logo val merged = suspendingMerge(avatar, logo) // 合并结果 // 👆 show(merged) // 更新 UI
}

可以看到,即便是比较复杂的并行网络请求,也能够通过协程写出结构清晰的代码。

开始使用协程

协程最简单的使用方法,其实在前面章节就已经看到了。我们可以通过一个 launch 函数实现线程切换的功能:

coroutineScope.launch(Dispatchers.IO) { ...
}

这个 launch 函数,它具体的含义是:我要创建一个新的协程,并在指定的线程上运行它。这个被创建、被运行的所谓「协程」是谁?就是你传给 launch 的那些代码,这一段连续代码叫做一个「协程」。

协程中却有一个很实用的函数:withContext 。这个函数可以切换到指定的线程,并在闭包内的逻辑执行结束之后,自动把线程切回去继续执行。那么可以将上面的代码写成这样:

coroutineScope.launch(Dispatchers.Main) { // 👈 在 UI 线程开始 val image = withContext(Dispatchers.IO) {  // 👈 切换到 IO 线程,并在执行完成后切回 UI 线程 getImage(imageId) // 👈 将会运行在 IO 线程 } avatarIv.setImageBitmap(image) // 👈 回到 UI 线程更新 UI
}

这种写法看上去好像和刚才那种区别不大,但如果你需要频繁地进行线程切换,这种写法的优势就会体现出来。可以参考下面的对比:

// 第一种写法
coroutineScope.launch(Dispachers.IO) { ... launch(Dispachers.Main){ ... launch(Dispachers.IO) { ... launch(Dispacher.Main) { ... } } }
}

// 通过第二种写法来实现相同的逻辑
coroutineScope.launch(Dispachers.Main) { ... withContext(Dispachers.IO) { ... } ... withContext(Dispachers.IO) { ... } ...
}

由于可以"自动切回来",消除了并发代码在协作时的嵌套。由于消除了嵌套关系,我们甚至可以把 withContext 放进一个单独的函数里面:

launch(Dispachers.Main) { val image = suspendingGetImage(imageId) avatarIv.setImageBitmap(image) } suspend fun suspendingGetImage(imageId: String) { withContext(Dispatchers.IO) { getImage(imageId) }

}

withContext 是一个 suspend 函数,它需要在协程或者是另一个 suspend 函数中调用。

suspend 是 Kotlin 协程最核心的关键字,几乎所有介绍 Kotlin 协程的文章和演讲都会提到它。它的中文意思是「暂停」或者「可挂起」。如果你去看一些技术博客或官方文档的时候,大概可以了解到:「代码执行到 suspend 函数的时候会『挂起』,并且这个『挂起』是非阻塞式的,它不会阻塞你当前的线程。」

协程中「挂起」的对象到底是什么?挂起线程,还是挂起函数?都不对,我们挂起的对象是协程。

还记得协程是什么吗?启动一个协程可以使用 launch,协程其实就是这个函数中闭包的代码块。

launch 创建的协程,在执行到某一个 suspend 函数的时候,这个协程会被「suspend」,也就是被挂起。

那此时又是从哪里挂起?从当前线程挂起。换句话说,就是这个协程从正在执行它的线程上脱离。

注意,不是这个协程停下来了!是脱离,当前线程不再管这个协程要去做什么了。

suspend 是有暂停的意思,但我们在协程中应该理解为:当线程执行到协程的 suspend 函数的时候,暂时不继续执行协程代码了。

我们先让时间静止,然后兵分两路,分别看看这两个互相脱离的线程和协程接下来将会发生什么事情:

线程:

前面我们提到,挂起会让协程从正在执行它的线程上脱离,具体到代码其实是:

协程的代码块中,线程执行到了 suspend 函数这里的时候,就暂时不再执行剩余的协程代码,跳出协程的代码块。

那线程接下来会做什么呢?

如果它是一个后台线程:

  • 要么无事可做,被系统回收
  • 要么继续执行别的后台任务

跟 Java 线程池里的线程在工作结束之后是完全一样的:回收或者再利用。

如果这个线程它是 Android 的主线程,那它接下来就会继续回去工作。

当这个协程被挂起的时候,就是主线程 post 的这个 Runnable 提前结束,然后继续执行它界面刷新的任务。

关于线程,我们就看完了。 这个时候你可能会有一个疑问,那 launch 包裹的剩下代码怎么办?

所以接下来,我们来看看协程这一边。

协程:

线程的代码在到达 suspend 函数的时候被掐断,接下来协程会从这个 suspend 函数开始继续往下执行,不过是在指定的线程

谁指定的?是 suspend 函数指定的,比如我们这个例子中,函数内部的 withContext 传入的 Dispatchers.IO 所指定的 IO 线程。

Dispatchers 调度器,它可以将协程限制在一个特定的线程执行,或者将它分派到一个线程池,或者让它不受限制地运行,关于 Dispatchers 这里先不展开了。

suspend 函数执行完成之后,协程为我们做的最爽的事就来了:会自动帮我们把线程再切回来

这个「切回来」是什么意思?

我们的协程原本是运行在主线程的,当代码遇到 suspend 函数的时候,发生线程切换,根据 Dispatchers 切换到了 IO 线程;

协程在执行到有 suspend 标记的函数的时候,会被 suspend 也就是被挂起,而所谓的被挂起,就是切个线程;

不过区别在于,挂起函数在执行完成之后,协程会重新切回它原先的线程

再简单来讲,在 Kotlin 中所谓的挂起,就是一个稍后会被自动切回来的线程调度操作

我们了解到了什么是「挂起」后,再接着看看这个「挂起」是怎么做到的。

通过 withContext 源码可以知道,它本身就是一个挂起函数,它接收一个 Dispatcher 参数,依赖这个 Dispatcher 参数的指示,你的协程被挂起,然后切到别的线程。

所以这个 suspend,其实并不是起到把任何把协程挂起,或者说切换线程的作用。

真正挂起协程这件事,是 Kotlin 的协程框架帮我们做的。

suspend 的意义?

这个 suspend 关键字,既然它并不是真正实现挂起,那它的作用是什么?

它其实是一个提醒。

什么是「非阻塞式挂起」

不卡线程

就像视频里讲的,阻塞不阻塞,都是针对单线程讲的,一旦切了线程,肯定是非阻塞的,你都跑到别的线程了,之前的线程就自由了,可以继续做别的事情了。

挂起就是可以自动切回来的切线程;

非阻塞式指的是它能用看起来阻塞的代码写出非阻塞的操作,并没有相对于线程相对高效的地方;

那协程和线程到底是什么关系呢?

协程就是基于线程而实现的一套更上层的工具API,类似于Android的HandlerAPI,基于线程的上层框架,

谣言:

说是协程是用户态的的,协程的切换不需要和操作系统进行交互,因此切换成本要比线程低

协程是协作式的,所以不需要线程的同步操作,这些描述对于有些语言来说是对的,但是对于Kotlin来说都是错的

协程是轻量级线程也是错误的

在Kotlin中协程的本质还是线程

本文参考链接:https://juejin.im/post/6844903949686800392#heading-0

勿删,copyright占位
分享文章到微博
分享文章到朋友圈

上一篇:CentOS:安装配置Scala、IDEA

下一篇:Python编程基础 Datawhale Task07:类、对象与魔法方法(3天)

您可能感兴趣

  • 深度学习PyTorch,TensorFlow中GPU利用率较低,CPU利用率很低,且模型训练速度很慢的问题总结与分析

            在深度学习模型训练过程中,在服务器端或者本地pc端,输入nvidia-smi来观察显卡的GPU内存占用率(Memory-Usage),显卡的GPU利用率(GPU-util),然后采用top来查看CPU的线程数(PID数)和利用率(%CPU)。往往会发现很多问题,比如,GPU内存占用率低,显卡利用率低,CPU百分比低等等。接下来仔细分析...

  • 【Java学习】-----基础(5)

    Java学习 接着上次的学习,构造方法学习完成后,我们将进入到两种及其常见的类 Scanner 和 Random 首先学习Scanner Scanner类的功能,可以实现键盘的输入数据,到程序中。 引用类型的...

  • Web Service入门学习2--1个示例

    本节创建一个Web Services示例: 本文JDK为1.8 (1)服务段代码 在Eclipse中新建一个Java项目,项目名称为webService,最终结构如下图: 创建一个Function类,其代码如下 package com.ws.services; import javax.jws.WebService; import javax...

  • centos 零碎学习小记 3.

    今天带来设置X shell 密钥登录   因为用的中文x shell 所以按照中文设置很简单1.点击工具(Tools)------新建用户密钥生成向导(New User key Wizard)(附图) 2.然后出现下图时,记得密钥类型 选择RSA.其实选择DSA ,也能实现,二者其实是不同的算法。(我用的RSA)实验的 ...

  • centos 零碎学习小记 1.

    dhclient  通过dhcp自动获取IP 2.uname -r     输出结果 2.6.32-358.e16.i686    2主版本号  6次版本号(奇数开发 偶数正式版)  32 修订次数  uname -i/-a (扩展) 其他选项可以使用 --help或者man获取的 3.linux 常用的几个...

  • python学习记录七(高级函数)

    python3中有很多比较别致的函数定义方式,让没有见过的同学总是很头痛,这一堆表达式到底是想干什么呀ヽ(#`Д´)ノ,本篇就总结一下python中出现的别致函数定义 lambda 表达式 定义:创建一个匿名函数对...

  • C++(学习) ——new与delete 运行过程 & operator new与operator delete的重载

    一、operator new C++可以利用new与delete来分配空间与释放空间,new与delete在调用时,其内部其实包含了三个步骤: 假如T代表一个类型(T可以表示内置类型,自定义类型(class)...

  • 18LaTeX学习系列之---LaTeX的参考文献

    目录 目录前言(一)简单的参考文献 1.说明2.源代码3.输出效果(二)以文件管理的方式 1.说明:2.源代码:3.输出效果(三)直接从源网站获取 1.说明2.操作 目录 本系列是有关LaTeX的学习系列,共计19篇,本章节是第18篇。 前一篇:17LaTeX学习系...

CSDN

CSDN

中国开发者社区CSDN (Chinese Software Developer Network) 创立于1999年,致力为中国开发者提供知识传播、在线学习、职业发展等全生命周期服务。

华为云40多款云服务产品0元试用活动

免费套餐,马上领取!
Kotlin学习之协程介绍:华为云为您免费提供Kotlin学习之协程在博客、论坛、帮助中心等栏目的相关文章,同时还可以通过 站内搜索 查询更多Kotlin学习之协程的相关内容。| 移动地址: Kotlin学习之协程 | 写博客