白话kotlin协程

news/2025/4/3 1:14:49/文章来源:https://www.cnblogs.com/WoodJim/p/18804801

文章同步发布于公众号:移动开发那些事白话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 = 0override 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 协程

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.hqwc.cn/news/909652.html

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

Javascript - 3

Javascript - 3 背后的运行原理High levelGarbage-collectedjs引擎内部的算法,为了不被不必要的东西堵塞,会从计算机内存中 自动删除旧的、未使用的对象解释型的 / 即时编译的语言(为了更快做出的调整) interpreted or just-in-time compiled通过 解释器(Interpreter) 逐行…

GPU学习笔记

从引言中“Dennard Scaling”的失效开始,引入GPU出现的背景,又介绍了GPU的通用性,以及高并发、低延迟保证的高计算速度。随后,我们以最常见的CUDA为例,介绍了GPU编程的基础,SIMT与SIMD,编译链接的过程。最后,我们深入硬件层面,分为三步走,先用最简系统“run起来”,然…

信创邮箱了解一下

企业微信后台,邮箱模块突然多了 【信创邮箱】这模块,让我们了解一下信创邮箱。一、信创邮箱的定义 信创邮箱,全称“信息技术应用创新邮箱”,是指基于信创全生态打造的企业邮箱服务。它遵循“信息技术安全创新”(简称“信创”)原则,采用国内自主研发的技术和方案实现信息…

小了 60,500 倍,但更强;AI 的“深度诅咒”

作者:Ignacio de Gregorio图片来自 Unsplash 的 Bahnijit Barman几周前,我们看到 Anthropic 尝试训练 Claude 去通关宝可梦。模型是有点进展,但离真正通关还差得远。 但现在,一个独立的小团队用一个只有一千万参数的模型通关了宝可梦,比主流前沿 AI 模型小了几千倍。 举个…

百度推出端到端语音大模型,支持方言、可打断、电话语音成本砍半;雷神 AI 眼镜发布:语音助理+摄像头,1799 元起丨日报

开发者朋友们大家好:这里是 「RTE 开发者日报」 ,每天和大家一起看新闻、聊八卦。我们的社区编辑团队会整理分享 RTE(Real-Time Engagement) 领域内「有话题的 技术 」、「有亮点的 产品 」、「有思考的 文章 」、「有态度的 观点 」、「有看点的 活动 」,但内容仅代表编辑…

技术书《AI芯片开发核心技术详解》(1)、《智能汽车传感器:原理设计应用》(2)、《TVM编译器原理与实践》(3)、《LLVM编译器原理与实践》(4),谢谢

4本书推荐《AI芯片开发核心技术详解》、《智能汽车传感器:原理设计应用》、《TVM编译器原理与实践》、《LLVM编译器原理与实践》由清华大学出版社资深编辑赵佳霓老师策划编辑的新书《AI芯片开发核心技术详解》已经出版,京东、淘宝天猫、当当等网上,相应陆陆续续可以购买。该…

从准备到成交的全方位销售策略

在销售领域,成功的关键在于一系列精心策划与高效执行的步骤。以下这份销售实战指南,全面涵盖从前期准备到最终成交的全流程,旨在助力销售人员提升业绩,构建稳固的客户关系。一、充分筹备1. 夯实产品知识深入掌握产品卖点、功能、价格,并做好竞品分析。确保在与客户沟通时,…

[T.2] 团队项目:选题和需求分析

Study The Spire :卡牌肉鸽关于这款游戏​ Study The Spire 是一款富有创意的 Rougelike 卡牌游戏,将随机与卡牌构筑要素融入游戏,选择不同卡牌来构筑独特卡组来战胜敌人,精心规划你的路线,获得丰富的游戏体验,在游戏过程中你也会逐渐提升自己的专业领域知识,休闲的同时…

高性能计算-GPU并行扫描

1. 扫描概念对数组arr[N]扫描就是得到数组prefix[N],每个元素是之前arr元素的求和. 开扫描定义:prefix1[N] = { arr[0], arr[0]+arr[1], ..., arr[0]+arr[1]+arr[N-1] } 闭扫描定义: prefix2[N] = { 0, arr[0], arr[0]+arr[1], ..., arr[0]+arr[1]+arr[N-12}2. Hillis steele …

硬盘损坏盘片划伤还能恢复数据资料吗?专业数据恢复公司怎么修复

这是一块台式机电脑里的希捷4T机械硬盘,型号是ST4000DM004,是北京某单位客户寄过来的,描述说是突然损坏不识别,出现了吱吱的异响声,先在北京当地找了一家数据恢复公司做了开盘维修处理,说是盘片有损伤,难度很大,在客户前后多次支付备件服务费后,也仅仅做出了100G左右的…

B+树是如何进行查询的?

千里之行,始于足下。 —— 老子因为一个数据页中的记录是有限的,且主键值是有序的,所以通过对所有记录进行分组,然后将组号(槽号)存储到页目录,使其起到索引作用,通过二分查找的方法快速检索到记录在哪个分组,来降低检索的时间复杂度。 但是,当我们需要存储大量的记录…

使用RAGFlow和Docker部署本地知识库

随着DeepSeek的火热,大模型对与广大开发者越来越触手可及。本文介绍了使用 DeepSeek R1 模型来构建本地知识库。Ollama 管理本地模型首先要了解大模型的管理工具 Ollama:安装 OllamaOllama 官网   点击下载安装 Ollama 客户端;安装成功后打开,在终端中输入以下命令来检查是…