白话kotlin协程
文章同步发布于公众号:移动开发那些事白话kotlin协程
1 什么是协程
Kotlin协程(Coroutine
)是一种轻量级的线程管理框架,允许开发者以更简洁,更高效的方式处理异步操作,避免回调地狱和线程阻塞,它有几个核心特性:
- 挂起与恢复
suspend
:可在耗时操作时挂起,释放线程资源,完成后自动恢复; - 非阻塞式并发:通过协作式调度实现任务的切换;
- 结构化并发:通过作用域自动管理协程生命周期;
2 协程的使用
2.1 协程的启动和调度
一般需要一个协程的作用域来启动和管理协程,然后在作用域里
使用launch
或者async
函数启动协程,使用suspend
来挂起协程:
launch
: 用于非阻塞的异步任务;async
: 用于可能返回结果的异步任务
fun main() = runBlocking {
// 在默认的环境里启动协程
val scope = CoroutineScope(Dispatchers.Default)
scope.launch {
// 在这里执行异步任务
val result = requestData();
}
val result = scope.async {
// 执行可返回结果的异步任务
}
// result.await() 拿到具体的结果
}
// suspend关键字,表示这个方法在协程域内调用,不会阻塞线程
suspend fun requestData():Map {
//实际的执行
}
并且在协程的使用过程中,可通过Dispatchers
来指定协程运行在哪个环境(这里的环境主要是指运行在哪个线程池下,不同的线程池有不同的处理策略):
Main
: 主线程IO
: 网络/文件操作Default
: CPU密集型计算
例:
// 在IO环境下启动协程
val result = async(Dispatchers.IO) {
执行异步操作
}
2.2 协程的取消
协程的取消可使用cancle
的关键字来处理。在启动协程时,会返回一个Job
对象,在需要取消时,可调用job.cancel
来达到这个目的;
val job: Job = GlobalScope.launch {
// 执行异步操作
}
// 取消
job.cancel()
2.3 协程的异常处理
异常的处理可使用自定义的CoroutineExceptionHandler
或者try-catch
块来实现;
val task1 = launch(CoroutineExceptionHandler { _, e ->
logError(e)
}) { /* ... */ }
try {
task1.join()
} catch (e: CancellationException) {
// 处理取消异常
}
3 协程底层实现原理
协程底层是基于协程的生命周期状态来处理的,协程的生命周期包括:
New
: 创建但未启动时的初始态;Active
Running
: 正在执行代码,会占用线程资源Suspended
: 执行suspend
函数时,释放线程,等待恢复;
Completed
:完成状态(正常完成或者异常完成)Cancelling
: 取消状态,进入资源清理阶段,但可执行finally
代码块;
3.1 核心原理
其核心原理是基于状态机。当遇到一个挂起函数时,就会将协程的代码置换为一个状态机,如代码里,有一个挂起函数:
suspend fun doSomething() {
delay(1000) // 挂起函数
println("Something done")
}
fun main() = runBlocking {
launch {
doSomething()
}
println("Main function continues")
}
编译器会将上面的代码编译成:
// 简化的状态机代码示意
class DoSomethingCoroutine : Continuation<Unit> {
// 当前状态
var state = 0
override fun resumeWith(result: Result<Unit>) {
when (state) {
0 -> {
state = 1
// 调用 delay 函数并传入当前协程作为 continuation
// delay函数执行完成后,会调用resumeWith方法
delay(1000, this)
}
1 -> {
println("Something done")
}
}
}
}
在挂起的异步操作完成后,会调用协程的resumeWith
方法,将结果传递给协程,协程会从暂停的位置恢复执行,并根据状态机的状态继续执行后续的代码;
3.2 调度器(CoroutineDispatcher
)原理
协程调度器负责决定协程在哪个线程或者线程池上使用;
- Default:用于CPU密集型,默认使用一个线程池
- IO: 专门的线程池
- Main: 用于在主线程上执行协程,通常用于更新UI;
3.2.1 Default
Dispatchers.Default
使用了一个基于ForkJoinPool
的线程池。ForkJoinPool
是 Java 7 引入的一种特殊线程池,它采用工作窃取算法(Work-Stealing Algorithm),可以高效地处理大量的小任务。
当一个协程通过 Dispatchers.Default
调度执行时,dispatch
方法会将协程任务封装成一个 Runnable
对象,并将其提交到 ForkJoinPool
中。ForkJoinPool 会从线程池中选择一个空闲的线程来执行该任务。
3.2.2 IO
Dispatchers.IO
也使用了一个线程池,不过这个线程池的大小可以根据系统资源动态调整。它的目的是为了处理大量的 I/O 阻塞操作,避免阻塞其他协程的执行。
当一个协程通过 Dispatchers.IO
调度执行时,dispatch
方法会将协程任务封装成一个 Runnable
对象,并将其提交到 IO 线程池中。由于 I/O 操作通常会阻塞线程,IO 线程池会有足够的线程来处理这些阻塞操作,从而保证其他协程可以继续执行。
3.2.3 Main
当一个协程通过 Dispatchers.Main
调度执行时,dispatch
方法会将协程任务封装成一个Runnable
对象,并通过Handler
将其发送到主线程的消息队列中。主线程的消息循环会依次取出消息队列中的任务并执行。
4 Flow
Flow
是 kotlin协程中的响应式编程,基于协程构建,主要用于处理异步数据流,并且是冷流,只在被收集时才会开始发送元素(同时,Flow
具有背压机制用于处理生产者和消费者的速度不匹配的问题),其有几个关键的组件:
- Flow 接口: 表示一个冷流,只有在被收集时才会开始发射元素,并且提供了一系列的操作符用于数据流的转换和处理;
- FlowCollector:用于收集
Flow
发射的元素; - FlowBuilder:用于构建Flow对象,常见的构建方式有:
flow
,flowOf
,asFlow
4.1 常见的应用场景
- 异步数据流
- UI数据更新(数据以Flow的形式暴露给UI,数据发生变化时,UI可以自动更新)
- 事件处理(将各种事件转换为Flow进行处理)
val dataFlow:Flow<String> = flow {
emit("data")
}
// 默认不使用背压机制,当速度不匹配时,生产者会先暂停等前面的数据处理完再继续生产数据
dataFlow.collect{data ->
.....
}
4.2 背压机制
背压(Backpressure)是一种反馈机制,用于处理生产者产生数据的速度快于消费者处理数据的速度的情况。当消费者处理数据的能力有限时,如果生产者持续快速地产生数据,可能会导致消费者内存溢出或者系统资源耗尽。背压机制允许消费者向生产者反馈自身的处理能力,从而使生产者调整数据的产生速度,以达到生产者和消费者之间的平衡。
常见的几个背压操作符:
buffer
: 创建一个缓冲区,来不及消息的内容会放到缓冲区里;conflate
: 会丢弃缓冲区中未处理的数据,只保留最新的数据collectLatest
: 当有新数据时,会取消当前正在处理的数据,只处理最新的数据
val dataFlow:Flow<String> = flow {
emit("data")
}
// 指定使用buffer的背压策略
dataFlow.buffer()
.collect{data ->
.....
}
5 Channel
Channel
是 Kotlin 协程库中用于在协程之间进行通信的工具,类似于队列,支持一个或多个协程向其发送元素,也支持一个或多个协程从其中接收元素,可用于实现生产者 - 消费者模式,它有不同的创建方式:
- Channel<类型>(10) : 创建固定大小的有缓冲的channel
- Channel<类型>(Channel.RENDEZVOUS) :创建无缓冲的channel,发送和接收要同步
- Channel<类型>(Channel.UNLIMITED) :创建无限缓冲的channel,
一个简单的使用Channel
的示例:
fun main() = runBlocking {
// 初始化一个默认大小的channel;
val channel = Channel<Int>()
// 生产者协程
launch {
for (i in 1..5) {
// 通过send发送元素,如果缓冲区已满,该操作会挂起,直到有空间可用
channel.send(i)
println("Sent $i")
}
// 关闭 Channel,关闭后不能再发送数据,但可以继续接收数据
channel.close()
}
// 消费者协程
launch {
// 当channel关闭,并且没有更多元素时,这个循环会自动结束
for (element in channel) {
// 会通过receive()方法接收元素
println("Received $element")
}
println("Channel closed")
}
}
5.1 Channel的类型
5.1.3 带容量限制的缓冲 Channel
指定一个固定的容量,当缓冲区满时,发送操作会挂起。
// 创建一个容量为10的Channel
val channel = Channel<Int>(10)
5.1.3 无缓冲的 Channel(Channel.RENDEZVOUS)
发送者和接收者必须同时准备好,发送操作会挂起,直到有接收者接收元素;接收操作也会挂起,直到有发送者发送元素。这种类型适用于需要严格同步的场景。
// 创建一个无缓冲的Channel
val channel = Channel<Int>(Channel.RENDEZVOUS)
5.1.3 无限缓冲的 Channel(Channel.UNLIMITED)
(默认创建)缓冲区可以容纳任意数量的元素,发送操作不会挂起。但需要注意,如果生产者速度远大于消费者速度,可能会导致内存占用过高。
// 创建一个不限制容量的Channel ,下面两个方法是等价的
val channel = Channel<Int>()
// val channel = Channel<Int>(Channel.UNLIMITED))
5.2 Channel底层原理
Channel
底层的核心是队列:
- 无缓冲的 Channel: 使用特殊队列,本身不存储元素,而是协调发送者和接收者的同步;
- 有缓冲的 Channel(指定容量):使用普通的
ArrayDeque
- 无限缓冲的 Channel(Channel.UNLIMITED) : 使用无界队列,如
LinkedList
;
6 参考
白话kotlin协程的更多相关文章
- Kotlin协程第一个示例剖析及Kotlin线程使用技巧
Kotlin协程第一个示例剖析: 上一次https://www.cnblogs.com/webor2006/p/11712521.html已经对Kotlin中的协程有了理论化的了解了,这次则用代码来直 ...
- Retrofit使用Kotlin协程发送请求
Retrofit2.6开始增加了对Kotlin协程的支持,可以通过suspend函数进行异步调用.本文简单介绍一下Retrofit中协程的使用 导入依赖 app的build文件中加入: impleme ...
- Kotlin协程基础
开发环境 IntelliJ IDEA 2021.2.2 (Community Edition) Kotlin: 212-1.5.10-release-IJ5284.40 我们已经通过第一个例子学会了启 ...
- Android Kotlin协程入门
Android官方推荐使用协程来处理异步问题.以下是协程的特点: 轻量:单个线程上可运行多个协程.协程支持挂起,不会使正在运行协程的线程阻塞.挂起比阻塞节省内存,且支持多个并行操作. 内存泄漏更少:使 ...
- Kotlin 协程一 —— 全面了解 Kotlin 协程
一.协程的一些前置知识 1.1 进程和线程 1.1.1基本定义 1.1.2为什么要有线程 1.1.3 进程与线程的区别 1.2 协作式与抢占式 1.2.1 协作式 1.2.2 抢占式 1.3 协程 二 ...
- rxjava回调地狱-kotlin协程来帮忙
本文探讨的是在tomcat服务端接口编程中, 异步servlet场景下( 参考我另外一个文章),用rxjava来改造接口为全流程异步方式 好处不用说 tomcat的worker线程利用率大幅提高,接口 ...
- Kotlin协程解析系列(上):协程调度与挂起
vivo 互联网客户端团队- Ruan Wen 本文是Kotlin协程解析系列文章的开篇,主要介绍Kotlin协程的创建.协程调度与协程挂起相关的内容 一.协程引入 Kotlin 中引入 Corout ...
- Kotlin协程通信机制: Channel
Coroutines Channels Java中的多线程通信, 总会涉及到共享状态(shared mutable state)的读写, 有同步, 死锁等问题要处理. 协程中的Channel用于协程间 ...
- Kotlin协程作用域与Job详解
Job详解: 在上一次https://www.cnblogs.com/webor2006/p/11725866.html中抛出了一个问题: 所以咱们将delay去掉,需要改造一下,先把主线程的dela ...
- Kotlin协程作用域与构建器详解
在上次我们是通过了这种方式来创建了一个协程: 接着再来看另一种创建协程的方式: 下面用它来实现上一次程序一样的效果,先来回顾一下上一次程序的代码: 好,下面改用runBlocking的方式: 运行一下 ...
随机推荐
- c# yield return
这个函数在处理循环时可以每生成一个数据就返回一个数据让主函数进行处理: static void Main(string[] args) { foreach (var item in GetNumber ...
- nginx.conf参数优化详解
1.Niginx主配置文件参数详解 a.上面博客说了在Linux中安装nginx.博文地址为:http://www.cnblogs.com/hanyinglong/p/5102141.html b.当 ...
- superset 1.3版本WIN10安装实录
首先说下,为什么要这么做,因为二开需要,二开要有源码,然后对源码修改,编译,所以不能通过类似https://zhuanlan.zhihu.com/p/271695878这种方式,直接安装: 1.去Gi ...
- C 2017笔试题
1.下面程序的输出结果是 int x=3; do { printf("%d\n",x-=2); }while(!(--x)); 输出:1 -2 解析:x初始值为3,第一次循环中运行 ...
- 2006. 差的绝对值为 K 的数对数目
给你一个整数数组 nums 和一个整数 k ,请你返回数对 (i, j) 的数目,满足 i < j 且 |nums[i] - nums[j]| == k . |x| 的值定义为: 如果 x &g ...
- Q:oracle通过正则表达式替换对应值
示例 把http://192.168.1.1:8888/a.html中的192.168.1.1:8888/替换成172.32.32.1:9999/ SELECT replace('http://192 ...
- 什么是Lambda架构?
一.简介 Lambda架构(Lambda Architecture)是由Twitter工程师南森·马茨(Nathan Marz)提出的大数据处理架构. 这一架构的提出基于马茨在BackType和Twi ...
- IDEA新建多模块maven项目
1.new =>projetc=>maven=>,新建完成后删除src目录 2.增加java模块 例:robots2-common 项目根目录就是[robots2-paren ...
- Luogu P10179 水影若深蓝 题解 [ 绿 ] [ 并查集 ] [ 构造 ]
水影若深蓝:挺好的一道并查集构造题. 观察 不难发现"距离为 \(2\)"这个条件我们可以通过黑白染色实现,我们把他们的中转点染成与他们相反的颜色,把这两个距离为 \(2\) 的点 ...
- Nginx~启动!!
前言 Nginx (engine x) 是一个高性能的HTTP和反向代理web服务器,同时也提供了IMAP/POP3/SMTP服务.Nginx是由伊戈尔·赛索耶夫为俄罗斯访问量第二的Rambler.r ...