Kotlin实战指南十三:协程
转载请标明出处: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
):
withContext
被 suspend
修饰,说明 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.Default
和 Dispatchers.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实战指南十三:协程相关推荐
- Kotlin实战指南二十:flow
转载请标明出处:http://blog.csdn.net/zhaoyanjun6/article/details/117370700 本文出自[赵彦军的博客] 文章目录 往期精彩文章 flow 是啥 ...
- Kotlin实战指南十八:open、internal 关键字使用
转载请标明出处:http://blog.csdn.net/zhaoyanjun6/article/details/117365712 本文出自[赵彦军的博客] 文章目录 往期精彩文章 open关键字 ...
- Kotlin实战指南十七:JvmField、JvmStatic使用
转载请标明出处:http://blog.csdn.net/zhaoyanjun6/article/details/116668666 本文出自[赵彦军的博客] 文章目录 往期精彩文章 @JvmFiel ...
- Kotlin实战指南十九:use 函数魔法
转载请标明出处:http://blog.csdn.net/zhaoyanjun6/article/details/117366756 本文出自[赵彦军的博客] 文章目录 往期精彩文章 use函数 往期 ...
- 《Kotlin 程序设计》第十二章 Kotlin的多线程:协程(Coroutines)
第十二章 Kotlin的多线程:协程(Coroutines) Kotlin 1.1 introduced coroutines, a new way of writing asynchronous, ...
- Kotlin开发利器之协程
Kotlin开发利器之协程 协程的定义 协程的开发人员 Roman Elizarov 是这样描述协程的:协程就像非常轻量级的线程.线程是由系统调度的,线程切换或线程阻塞的开销都比较大.而协程依赖于 ...
- Kotlin实战指南十四:协程启动模式
转载请标明出处:https://blog.csdn.net/zhaoyanjun6/article/details/96008400 本文出自[赵彦军的博客] 文章目录 协程启动 DEFAULT LA ...
- Kotlin实战指南十五:协程泄漏
转载请标明出处:https://blog.csdn.net/zhaoyanjun6/article/details/106413283 本文出自[赵彦军的博客] 文章目录 协程泄漏的本质 Global ...
- Kotlin学习笔记26 协程part6 协程与线程的关系 Dispatchers.Unconfined 协程调试 协程上下文切换 Job详解 父子协程的关系
参考链接 示例来自bilibili Kotlin语言深入解析 张龙老师的视频 1 协程与线程的关系 import kotlinx.coroutines.* import java.util.concu ...
最新文章
- LDAP(轻量目录存取协议)
- TCP为什么是3次握手?
- 71道Android开发面试题
- DataTransmission:免费薅羊毛,Are you kidding me? 镭速传输 “百日计划”提前大曝光!Raysync传输协议要开放?
- chrome 悬停大图插件_Google Chrome浏览器的悬停卡:我不想要的我最喜欢的新东西
- LeetCode MySQL 570. 至少有5名直接下属的经理
- Docker容器数据卷讲解
- 案例:按照JSP Model2思想实现用户注册功能
- 翻译连载 | JavaScript轻量级函数式编程-第5章:减少副作用 |《你不知道的JS》姊妹篇...
- Moodle中的角色与权限控制
- c语言发牌小游戏,大家想想怎么用c实现我们经常玩的斗地主游戏的发牌过程呢?...
- Emacs显示函数列表imenu-list
- webpack@3.6.0(4) -- 配置模块化开发
- python清空字典保留变量_python中字典删除元素
- 在MySQL中以下属于ddl语句的_ddl语言(以下哪些命令是ddl语句)
- QT应用编程: 编写HC05串口蓝牙调试助手(Android系统APP)
- python 获得时间戳_Python 获取时间戳
- 自考学习记录 课程代码03708《中国近代史纲要》1
- 基于SpringBoot的外卖点餐管理系统
- 〖Python 数据库开发实战 - Python与Redis交互篇⑯〗- 综合案例 - 新闻管理系统第二阶段完结 - “app.py“ 模块收尾及案例演示
热门文章
- linux ftp非隔离模式,FTP实验报告非隔离用户的使用教案.doc
- java跨用问题怎么解决_跨浏览器问题的五种解决方案
- mysql union all sum_[数据库]SQL Server UNION ALL 结果 SUM函数造成精度丢失
- 第一次接广告的心得,关于广告我怎么看
- 八十、React中的容器组件和无状态组件
- 五、Hive架构,安装和基本使用
- 区别于传统低效标注,两种基于自然语言解释的数据增强方法
- Funnel-Transformer:让Transformer更高效地处理长序列
- Fashion-MNIST数据集发布一周年,论文引用量超250篇
- Python 字典中get() 函数