Kotlin协程之协程的结构化详解

news2024/9/24 13:25:43

前言

上一篇文章介绍了协程概念的具像化,算是对协程的概念进一步进行加深,本篇文章来看一下协程结构化的概念。

结构化 是协程中一个非常重要且非常实用的一个特性,它可以帮助我们更好的管理协程的生命周期。

如果说 挂起函数 解决了并发任务的写法问题,使得我们可以像写同步代码一样来实现异步逻辑,消除了 回调地狱,降低了 并发任务的复杂度

那么协程的结构化 则帮我们解决了 并发任务的管理 的问题。

协程的父子关系

之所以说协程是结构化的,是因为协程是存在父子关系的。

一个协程可以有多个子协程,子协程又可以有多个子协程,这样就形成一个树形结构。

在上一篇协程概念具像化的文章中,我们分析了协程创建的过程,并提到了通过 coroutineScope 中的 launch 函数创建出来的协程的返回值类型是 Job。这个 Job中的大部分属性和方法都跟线程中的 Thread 类似,比如 startjoincancel 等。
但是,Job 还有线程没有对等概念的属性例如: parentchildren,而 parentchildren 就是体现协程结构化的关键,从名字上也可以看出来,parent 是父协程,children 是子协程。

先来看一段代码

fun main() = runBlocking {
    //创建一个协程作用域
    val scope = CoroutineScope(Dispatchers.IO)
    var childJob: Job? = null
    //启动一个协程
    val job = scope.launch {
        println("父协程")
        //启动一个子协程
        childJob = launch {
            delay(100)
            println("子协程")
        }
    }
    val childrenJob = job.children
    println("childrenJob.count = ${childrenJob.count()}")
    println("childrenJob.first() === childJob = ${childrenJob.first() === childJob}")
    println("childJob?.parent===job = ${childJob?.parent === job}")

    //等待协程结束
    job.join()
}

输出结果:
在这里插入图片描述

可以看到,我们通过 CoroutineScope 创建了一个协程作用域,然后通过 launch 函数启动了一个协程,再直接在 launch 代码块中又通过 launch 函数启动了一个子协程。
通过打印的结果可以看到 jobchildJob 是父子关系。

父子关系的建立

那么,这个父子关系是怎么建立的呢?

我们来跟下源码:

还是以 launch 函数为例,这个 launch 函数在 协程概念具像化 这篇文章中详细分析过了,这里就不说得太细了。

在这里插入图片描述
首先,launch 函数会创建一个 StandaloneCoroutine 对象,这个对象最终以 Job 的形式返回。

再来看看 StandaloneCoroutine

在这里插入图片描述
StandaloneCoroutine 继承自 AbstractCoroutine,注意这里传过去的参数是 parentContext,这个参数就是父协程的 contextinitParentJob 的值为 true。

再来看看 AbstractCoroutineinitParentJob 函数:

在这里插入图片描述

initParentJob 实际上就是用来建立父子关系的,它会将父协程的 Job 传递过去,在内部做处理,建立父子关系。
initParentJobJobSupport 类中的一个方法,该方法中做了两件事:

  1. parent.attachChild(this) :将当前协程添加到父协程的 children 属性中
  2. this.parent = parent :将父协程赋值给当前协程的 parent 属性

在这里插入图片描述

这样一来,父协程就可以通过 children 属性来获取自己的所有子协程了。而子协程也可以通过 parent 属性来获取自己的父协程。

也就是说,父子协程的建立是通过 Jobparentchildren 属性来实现的。决定协程父子关系的关键点是 Job 对象。

理清楚了父子关系的建立逻辑,我们回过头来想一下,最开始的父协程的 Job对象是哪来的呢?我在代码中并没有显式的创建父协程的 Job 对象啊。
再来回过头来看一下之前的代码:

在这里插入图片描述
我最开始是通过 CoroutineScope 创建了一个协程作用域,然后通过 launch 函数启动了一个协程,这个协程就是父协程。
那么,这个父协程的 Job 对象是怎么来的呢?

看下 CoroutineScope 的源码:
在这里插入图片描述

可以看到,如果我们没有显式的传递 Job 对象,那么 CoroutineScope 会自动创建一个 Job 对象,这个 Job
对象就是父协程的 Job 对象。

launch 中创建协程对象之前,会先执行 CoroutineScope.newCoroutineContext 方法。

在这里插入图片描述

这个方法会把 scope 中的 coroutineContext 也就是我们创建出来的 CoroutineScope 中的coroutineContextEmptyCoroutineContext 合并,生成一个新的 CoroutineContext,然后传递给 StandaloneCoroutine 的构造函数。

那这样一来,StandaloneCoroutine 就会拿到这个 CoroutineContext,然后通过 CoroutineContext 获取到 Job
对象,这个 Job 对象就是父协程的 Job 对象。

至此,Job 是怎么来的,以及父子关系是怎么建立的,我们都已经分析清楚了。

下面看一下多个父子关系的情况来巩固一下:


fun main() = runBlocking {
    //创建一个协程作用域
    val scope = CoroutineScope(Dispatchers.IO)

    var childJob: Job? = null
    var childJob2: Job? = null
    var childJob3: Job? = null
//
    //启动一个协程,job 是由 CoroutineScope 创建的,所以job的父协程是scope
    val job = scope.launch {
        println("父协程")

        //启动一个子协程,childJob的父协程是job,也就是说job跟childJob是父子关系
        childJob = launch {

            //启动一个子协程,child3的父协程是childJob,也就是说childJob3跟childJob是父子关系
            childJob3 = launch {
                delay(100)
                println("子协程的子协程")
            }

            delay(100)
            println("子协程")
        }

        //启动一个子协程,childJob2的父协程是job,也就是说job跟childJob2是父子关系.childJob和childJob2是兄弟关系
        childJob2 = launch {
            delay(100)
            println("子协程2")
        }
    }
//    delay(200)
    val childrenJob = job.children

    val scopeChildrenJobs = scope.coroutineContext[Job]?.children
    println("scopeChildrenJobs.count = ${scopeChildrenJobs?.count()}")
    println("scopeChildrenJobs?.first()===job = ${scopeChildrenJobs?.first() === job}")

    println("childrenJob.count = ${childrenJob.count()}")
    println("childJob?.parent===job = ${childJob?.parent === job}")
    println("childJob2?.parent===job = ${childJob2?.parent === job}")
    println("childJob3?.parent===childJob = ${childJob3?.parent === childJob}")
    //等待协程结束
    job.join()
}

在上面的代码中:

  1. 通过 scope.launch 创建出来的 job,他的父 Job 对象是 scope 中的 Job 对象,因此,jobscope 的子协程。
  2. job 中通过 launch 创建出来的 childJob,他的父 Job 对象是 job,因此,childJobjob 的子协程。
  3. job 中通过 launch 创建出来的 childJob2,他的父 Job 对象是 job,因此,childJob2job 的子协程。
  4. childJob 中通过 launch 创建出来的 childJob3,他的父 Job 对象是 childJob,因此,childJob3childJob 的子协程。
  5. childJobchildJob2 是兄弟关系,他们的父 Job 对象都是 job

执行结果:

在这里插入图片描述

这样一来,他们就形成一个树形结构如下:

在这里插入图片描述

总结

  1. 协程是存在父子关系的,父协程可以有多个子协程,子协程也可以有多个子协程,形成一个树形结构。
  2. 父子关系是通过 Job 对象的 parentchildren 来维护的,Job 对象是确定父子关系的关键。

在我们日常写代码的时候,建议遵循以下原则:

  • 正常在一个协程中直接启动子协程,这样默认就是父子关系,方便管理。
  • 尽量不要在一个协程中启动不是它的子协程的协程,这样会导致协程的父子关系混乱,不利于协程的管理。

类似下面的代码就会破坏父子关系:

fun main() = runBlocking {
    //创建一个协程作用域
    val scope = CoroutineScope(Dispatchers.IO)
    var job1: Job? = null
    var job2: Job? = null

    val job = scope.launch {
        println("父协程")
        println("job:${coroutineContext.job}")
        //这里的协程虽然是在嵌套的launch中创建的,但是是通过scope创建的,因此,job1的父协程是也是scope,而不是job
        job1 = scope.launch {
            println("job1:${coroutineContext.job.parent}")
        }

        //这里虽然是在嵌套的launch中创建的,但是传递了一个新的Job,因此job2的父协程也不是job
        job2 = launch(Job()) {
            println("job2:${coroutineContext.job.parent}")
        }
    }
    val childrenJob = job.children
    println("childrenJob.count = ${childrenJob.count()}")
    //等待协程结束
    job.join()
}

输出结果:

在这里插入图片描述

可以看到,虽然代码结构是嵌套的,但是 job1job2 的父协程并不是 job,也就不存在父子关系了,这样的代码结构是不利于协程的管理的。

最后提一下上面的代码中为什么最后要调用 job.join()

runBlocking 函数是一个挂起函数,它会阻塞当前线程,直到他内部的所有子协程都执行完毕,但是我们写的代码是通过自己创建的 CoroutineScope 来启动协程的,也就是说,我们后续创建的协程都是 CoroutineScope 的子协程,而不是 runBlocking 的子协程,那么 runBlocking 并不会等待 CoroutineScope 中的协程执行完毕,所以我们需要手动调用 job.join() 来等待 CoroutineScope 中的协程执行完毕。

协程会等待所有的子协程执行完毕后才会结束,这也是协程结构化的一个体现。

后续关于协程的取消,异常处理等内容,都跟协程的父子关系有很强的关联。所以,协程结构化是协程中一个非常重要的知识点,一定要理清楚。

好了,这篇文章就到这里,希望对你有所帮助。


感谢阅读,如果对你有帮助请点赞支持。有任何疑问或建议,欢迎在评论区留言讨论。如需转载,请注明出处:喻志强的博客

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

今日leetCode 19.删除链表的倒数第N个结点

19. 删除链表的倒数第 N 个结点 给你一个链表,删除链表的倒数第 n 个结点,并且返回链表的头结点。 示例 1: 输入:head [1,2,3,4,5], n 2 输出:[1,2,3,5]示例 2: 输入:head [1], n 1 输出&…

每日OJ_牛客_简单错误记录(文件模拟)

目录 简单错误记录(文件模拟) 解析代码 简单错误记录(文件模拟) 简单错误记录__牛客网 开发一个简单错误记录功能小模块,能够记录出错的代码所在的文件名称和行号。 处理: 1、 记录最多8条错误记录&a…

说说精益生产管理咨询公司排名的那些事

面对市场上琳琅满目的精益生产管理咨询公司,企业如何做出明智选择,避免陷入“坑”中,成为了一个值得探讨的话题。本文将从多个维度出发,为大家揭晓精益生产管理咨询公司排名的那些事,助您找到最适合的合作伙伴。 一、认…

日本IT工作内容---一篇梗概

日本IT工作现场是一个充满活力、技术导向且多元化的工作环境。以下是对日本IT工作内容的详细概述: 1. 软件开发(Software Engineering, SE) 主要任务:编写、测试和维护软件,以满足客户需求。这包括使用多种编程语言和…

Tuxera NTFS for Mac破解版下载 Tuxera NTFS for Mac2023激活码 mac电脑ntfs磁盘软件

Tuxera NTFS for Mac是一款优秀的Mac系统完全读写软件,提供Fat32、NTFS、Exfat、mac os扩展格式的转换,稳定性好,传输速度极快。Tuxera NTFS for Mac功能丰富,能修复NTFS卷、创建NTFS磁盘映像、创建NTFS分区等等。同时软件支持所有…

英伟达25年收购案例盘点:从3dfx到Mellanox,如何通过技术并购建立护城河

英伟达(NVIDIA)作为全球领先的GPU制造商,近年来通过一系列高效的收购活动,不断扩大其技术布局,并稳固其在AI、数据中心等核心领域的行业领先地位。在过去25年中,英伟达通过软硬件全栈式收购策略&#xff0c…

Qt绘制动态仪表(模仿汽车仪表指针、故障灯)

背景: 项目需要,可能需要做一些仪表显示。此篇除了介绍实现方法,还要说明心路历程。对我而言,重要的是心理,而不是技术。写下来也是自勉。 本人起初心里是比较抵触的,从业20多年了,深知所谓界…

240911-基于Ollama的22行代码实现极简交互式RAG问答

A. 最终效果 B. 环境配置 pip install llama-index-embeddings-ollama pip install llama-index-llms-ollama pip install llama-indexC. 代码程序 from llama_index.core import VectorStoreIndex, Document, SimpleDirectoryReader,Settings from llama_index.llms.ollama …

计算机毕业设计选题推荐-动漫插画分享网站-Java/Python项目实战

✨作者主页:IT毕设梦工厂✨ 个人简介:曾从事计算机专业培训教学,擅长Java、Python、微信小程序、Golang、安卓Android等项目实战。接项目定制开发、代码讲解、答辩教学、文档编写、降重等。 ☑文末获取源码☑ 精彩专栏推荐⬇⬇⬇ Java项目 Py…

Linux 工程师:探索开源世界的专业之路

摘要: 本文深入探讨了 Linux 工程师这一职业角色。从 Linux 操作系统的基础特性与架构开始阐述,详细分析了 Linux 工程师在不同领域的工作范畴,包括系统安装与配置、内核开发与优化、网络管理、安全防护等方面。文中还探讨了成为一名优秀 Lin…

vue3数据持久化方案:pinia-plugin-persistedstate源码浅析

概述 Pinia是vue3的官方推荐用于数据共享的库,但是Pinia🍍中的数据是存在于浏览器的内存中,当浏览器刷新后,这些数据就会消失。因此我们需要对数据做持久化存储,这个时候就需要用到pinia-plugin-persistedstate。 pinia-plugin-persistedstate本质上利用浏览器持久化存…

即插即用篇 | YOLOv8 引入高效的直方图Transformer模块 | 突破天气障碍:Histoformer引领高效图像修复新路径“

本改进已同步到YOLO-Magic框架! 摘要:摘要。基于Transformer的恶劣天气图像修复方法取得了显著进展。大多数方法通过沿通道维度或在空间上固定范围的块内使用自注意力,以减少计算负担。然而,这种折中方式在捕获长距离空间特征方面存在局限性。受到恶劣天气导致的退化因素主…

C++职工管理系统(具备增删改查功能 涉及文件操作、指针数组操作、升序降序、多态、虚函数)

目录 🌕需求分析🌕创建项目🌕完整代码🌙项目结构🌙include⭐worker.h (它是后面employ,boss,manager的基类)⭐boss.h⭐employee.h⭐manager.h⭐workerManager.h 🌙src⭐boss.cpp⭐em…

0911(绘制事件,qt中的网络通信)

一、实现一个时钟 1)代码 头文件&#xff1a; #ifndef WIDGET_H #define WIDGET_H#include <QWidget> #include <QPainter> #include <QPaintEvent> #include <QTimer> #include <QTime> #include <QTimerEvent>QT_BEGIN_NAMESPACE nam…

如何用Google Trend进行SEO优化?方法与策略

做SEO的都知道&#xff0c;Google Trend是一款免费工具&#xff0c;用户可以查看不同关键词的搜索趋势、兴趣强度和区域分布。通过 Google Trends&#xff0c;你可以获得以下信息&#xff0c;这些数据可以帮助您更好地了解用户需求并优化您的SEO策略&#xff1a; 1、搜索量趋势…

线结构光测量系统标定--导轨

光平面标定原理可查看之前的博文《光平面标定》&#xff0c;光条中心提取可参考线结构光专栏光条中心提取系列的文章&#xff0c;相机标定参考相机标定专栏中的博文。&#xff08;欢迎进Q群交流&#xff1a;874653199&#xff09; 线结构光测量系统(指一个线结构光传感器与一个…

如何检查前端项目中我们没有使用的第三方包

问题描述&#xff1a;我们在赶项目或者在做些功能或者效果的时候往往会用到很多的第三方包&#xff0c;那么时间一长&#xff0c;我们有时候会忘记删除这些包到底该怎么办呢&#xff1f;接下来教给大家一个方法。 在我们的项目根目录下面随便起一个.js的文件 代码如下&#x…

算法工程师重生之第四天(两两交换链表中的节点 删除链表的倒数第N个节点 链表相交 环形链表II 总结 )

参考文献 代码随想录 一、两两交换链表中的节点 给你一个链表&#xff0c;两两交换其中相邻的节点&#xff0c;并返回交换后链表的头节点。你必须在不修改节点内部的值的情况下完成本题&#xff08;即&#xff0c;只能进行节点交换&#xff09;。 示例 1&#xff1a; 输入&am…

掌握python的dataclass,让你的代码更简洁优雅!

"dataclass"是从"Python3.7"版本开始&#xff0c;作为标准库中的模块被引入。 随着"Python"版本的不断更新&#xff0c;"dataclass"也逐步发展和完善&#xff0c;为"Python"开发者提供了更加便捷的数据类创建和管理方式。 …

Element-UI 组件实现面包屑导航栏

Element-UI 组件实现面包屑导航栏 面包屑导航栏是一种辅助导航系统&#xff0c;它显示用户当前位置在网站或应用层次结构中的位置&#xff0c;可以帮助用户了解他们当前页面的位置&#xff0c;并且可以方便地返回到上级页面或首页。 面包屑导航栏的实现原理&#xff1a; 路径…