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


文章目录

  • 前言-协程介绍
  • 主流语言对协程的支持
  • Android 项目引用
  • 创建一个协程
  • 取消协程工作
  • launch 参数详解
  • 线程调度器 Dispatchers
  • withContext
  • withContext 的性能
  • 综合演练
  • 协程到底是什么
  • 参考资料

前言-协程介绍

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

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

以上任意一点都是很消耗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? = nulloverride 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 threadfor (i in 10 downTo 1) { // countdown from 10 to 1tv1?.text = "Countdown $i ..." // update textdelay(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? = nullvar mCancelButton: Button? = nulloverride 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 threadfor (i in 10 downTo 1) { // countdown from 10 to 1tv1?.text = "Countdown $i ..." // update textdelay(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.Default CPU密集型任务 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.Mainval result = get("developer.android.com")// Dispatchers.Mainshow(result)
}// Dispatchers.Main
suspend fun get(url: String) =// Dispatchers.IOwithContext(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) {//网络请求成功以后,到主线程更新UILog.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: Booleanpublic val isCompleted: Booleanpublic val isCancelled: Booleanpublic fun start(): Booleanpublic fun cancel(cause: CancellationException? = null)public suspend fun join()...
}

我们再来看看 Thread 的定义:

public class Thread implements Runnable {...    public final native boolean isAlive();public synchronized void start() { ... }@Deprecatedpublic 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 , 欢迎关注

Kotlin实战指南十三:协程相关推荐

  1. Kotlin实战指南二十:flow

    转载请标明出处:http://blog.csdn.net/zhaoyanjun6/article/details/117370700 本文出自[赵彦军的博客] 文章目录 往期精彩文章 flow 是啥 ...

  2. Kotlin实战指南十八:open、internal 关键字使用

    转载请标明出处:http://blog.csdn.net/zhaoyanjun6/article/details/117365712 本文出自[赵彦军的博客] 文章目录 往期精彩文章 open关键字 ...

  3. Kotlin实战指南十七:JvmField、JvmStatic使用

    转载请标明出处:http://blog.csdn.net/zhaoyanjun6/article/details/116668666 本文出自[赵彦军的博客] 文章目录 往期精彩文章 @JvmFiel ...

  4. Kotlin实战指南十九:use 函数魔法

    转载请标明出处:http://blog.csdn.net/zhaoyanjun6/article/details/117366756 本文出自[赵彦军的博客] 文章目录 往期精彩文章 use函数 往期 ...

  5. 《Kotlin 程序设计》第十二章 Kotlin的多线程:协程(Coroutines)

    第十二章 Kotlin的多线程:协程(Coroutines) Kotlin 1.1 introduced coroutines, a new way of writing asynchronous, ...

  6. Kotlin开发利器之协程

    Kotlin开发利器之协程 协程的定义   协程的开发人员 Roman Elizarov 是这样描述协程的:协程就像非常轻量级的线程.线程是由系统调度的,线程切换或线程阻塞的开销都比较大.而协程依赖于 ...

  7. Kotlin实战指南十四:协程启动模式

    转载请标明出处:https://blog.csdn.net/zhaoyanjun6/article/details/96008400 本文出自[赵彦军的博客] 文章目录 协程启动 DEFAULT LA ...

  8. Kotlin实战指南十五:协程泄漏

    转载请标明出处:https://blog.csdn.net/zhaoyanjun6/article/details/106413283 本文出自[赵彦军的博客] 文章目录 协程泄漏的本质 Global ...

  9. Kotlin学习笔记26 协程part6 协程与线程的关系 Dispatchers.Unconfined 协程调试 协程上下文切换 Job详解 父子协程的关系

    参考链接 示例来自bilibili Kotlin语言深入解析 张龙老师的视频 1 协程与线程的关系 import kotlinx.coroutines.* import java.util.concu ...

最新文章

  1. LDAP(轻量目录存取协议)
  2. TCP为什么是3次握手?
  3. 71道Android开发面试题
  4. DataTransmission:免费薅羊毛,Are you kidding me? 镭速传输 “百日计划”提前大曝光!Raysync传输协议要开放?
  5. chrome 悬停大图插件_Google Chrome浏览器的悬停卡:我不想要的我最喜欢的新东西
  6. LeetCode MySQL 570. 至少有5名直接下属的经理
  7. Docker容器数据卷讲解
  8. 案例:按照JSP Model2思想实现用户注册功能
  9. 翻译连载 | JavaScript轻量级函数式编程-第5章:减少副作用 |《你不知道的JS》姊妹篇...
  10. Moodle中的角色与权限控制
  11. c语言发牌小游戏,大家想想怎么用c实现我们经常玩的斗地主游戏的发牌过程呢?...
  12. Emacs显示函数列表imenu-list
  13. webpack@3.6.0(4) -- 配置模块化开发
  14. python清空字典保留变量_python中字典删除元素
  15. 在MySQL中以下属于ddl语句的_ddl语言(以下哪些命令是ddl语句)
  16. QT应用编程: 编写HC05串口蓝牙调试助手(Android系统APP)
  17. python 获得时间戳_Python 获取时间戳
  18. 自考学习记录 课程代码03708《中国近代史纲要》1
  19. 基于SpringBoot的外卖点餐管理系统
  20. 〖Python 数据库开发实战 - Python与Redis交互篇⑯〗- 综合案例 - 新闻管理系统第二阶段完结 - “app.py“ 模块收尾及案例演示

热门文章

  1. linux ftp非隔离模式,FTP实验报告非隔离用户的使用教案.doc
  2. java跨用问题怎么解决_跨浏览器问题的五种解决方案
  3. mysql union all sum_[数据库]SQL Server UNION ALL 结果 SUM函数造成精度丢失
  4. 第一次接广告的心得,关于广告我怎么看
  5. 八十、React中的容器组件和无状态组件
  6. 五、Hive架构,安装和基本使用
  7. 区别于传统低效标注,两种基于自然语言解释的数据增强方法
  8. Funnel-Transformer:让Transformer更高效地处理长序列
  9. Fashion-MNIST数据集发布一周年,论文引用量超250篇
  10. Python 字典中get() 函数