抽丝剥茧聊Kotlin协程之协程是如何建立结构化并发的

2023-11-08 22:10

本文主要是介绍抽丝剥茧聊Kotlin协程之协程是如何建立结构化并发的,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

1. 协程的结构化并发

上篇文章,我通过两个例子简单的介绍了Job cancel方法在不同的父子关系情况下,看起来很相似的代码,执行结果却很不相同的情况。文中我引出了Job结构化并发的概念,即父Job和子Job形成树的数据结构,本文我将详细介绍Kotlin协程框架是如何实现结构化并发的。

什么叫结构化并发?用通俗易懂的话解释就是,协程之间的协作是有组织,有纪律的。如果协程之间的关系是确定了的,那么协程之间的cancel和exception处理是有章法可寻的。

下图,假设Job2处发生了异常,那么它的子Job、父Job以及父Job的其它子Job是否会被cancel掉呢?答案是看情况(不是说有章法可寻吗?怎么成看情况了?因为算法是固定的,但是Job的行为可能各不相同)。在树的数据结构中,任何一个节点的cancel事件有两条传播路径,向上传播给父节点,向下传播给子节点。那么接受到cancel事件的父节点或子节点是否cancel掉自己完全取决于它的内部实现。

举例说明,假设Job2处发生异常了协程被迫cancel掉,事件传播到Job0。那么Job0会有两种处理方式。其一、把自己也cancel掉,然后把自己的所有子Job都cancel掉,其二、忽略掉该事件,当作啥事也没发生。

熟悉协程的同学应该能意识到,这两种处理方式分别对应Job和SupervisorJob。

1.1 Job处理异常

job2处1/0发生异常打印结果如下,我们注意到通过Job方式启动,随着job2发生异常,job1和job3都被cancel掉。

2021-12-19 16:35:52.439  job1 start
2021-12-19 16:35:52.440  job2 start
2021-12-19 16:35:52.440  job3 start

1.2 SupervisorJob处理异常

打印结果如下,job1和job3不受job2的异常影响,程序照常执行。

2021-12-19 16:44:30.832  job1 start
2021-12-19 16:44:30.833  job2 start
2021-12-19 16:44:30.833  job3 start
2021-12-19 16:44:32.835  job1 end
2021-12-19 16:44:32.835  job3 end

那么问题来了:为什么SupervisorJob和Job会有这样的差异呢?

2. Job基础知识

通过CoroutineScope.launch方法启动协程,返回值为Job类型。本文着重讲解以下几个方法:

  1. start
  2. cancel
  3. invokeOnCompletion

2.1 start方法

start方法作用是启动一个协程,类似Thread.start方法。通过默认的方式启动协程,start方法会默认被调用的。

2.2 cancel方法

cancel方法作用是取消协程,避免资源的浪费,Android开发者比较熟悉的场景有,当一个Activity销毁之后,应该取消掉还没有执行完的工作。协程中的cancel,必须要有挂起点。

2.2.1 使用delay方法

观察日志由于delay方法是suspend修饰的,它是协程的挂起点,发现job1被成功cancel掉。

2021-12-19 17:16:16.377  running in job2

2.2.2 使用Thread.sleep方法
观察发现,job1协程体没有调用suspend修饰的方法,无法被cancel掉,job1

2021-12-19 17:26:05.554  running in job2
2021-12-19 17:26:06.544  running in job1

2.2.3 通过yield或者ensureActive方法更正

在Thread.sleep方法后面增加yield或者ensureActive方法,可以让job1被cancel掉。yield本身是一个suspend函数,在yield方法处恢复协程体运行时,会检查当前协程是否被cancel掉。ensureActive则是通过抛出CancellationException取消掉当前的协程。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-BTVo6GfP-1640569487317)(https://cdn.jsdelivr.net/gh/lizijin/bytestation2@master/coroutines/aj4.png)]

2.2.4 CancellationException是一种特殊的Exception,它和Exception的区别在于,如果在协程中抛出CancellationException,除了子协程外并不会影响其它协程


对比1.1代码1/0抛出除0异常,CancellationException并不会影响job1和job3。

2021-12-19 19:08:46.558  job1 start
2021-12-19 19:08:46.559  job2 start
2021-12-19 19:08:46.561  job3 start
2021-12-19 19:08:46.626  job4 start
2021-12-19 19:08:48.560  job1 end
2021-12-19 19:08:48.561  job3 end

2.3 invokeOnCompletion方法

该方法的作用是给Job注册一个回调,当Job执行完成后执行回调函数。

2021-12-19 19:19:39.278  job start
2021-12-19 19:19:41.280  job end
2021-12-19 19:19:41.281  job invokeOnCompletion

如果协程被cancel掉了呢?

当协程被cancel时,回调立马执行。

2021-12-19 19:34:21.809  job start
2021-12-19 19:34:22.811  job invokeOnCompletion

我们来看下invokeOnCompletion源码

CompletionHandler类似Java中的callback,当Job完成后,调用该回调,类似Android的OnClickListener。CompletionHandler持有ChildJob,并注册到父Job上,那么子Job就可以监听父Job的取消和完成事件,从而实现事件从上往下传播。如果子Job能够直接或间接的持有父Job的引用,那么当子Job被cancel时,就能直接把事件,从下往上传播。所以cancel事件是双向传播的

而协程框架也正是通过invokeOnCompletion方法建立起父子Job的树形关系

3. Job建立树形关系



我们关注上面类图标红的几个类。

划重点


  1. ChildHandle的childCancelled表示子Job被取消时,往上传播
  2. CompletionHandler的invoke方法表示父Job被取消时执行子Job的回调,往下传播,最终会调用到ChildJob的parentCancelled方法
  3. ChildHandleNode同时实现了ChildHandle和CompletionHandler,表示该节点可以双向传播

核心方法有:

  1. JobSupport.initParentJobInternal(parent: Job?)
  2. Job.attachChild(child: ChildJob): ChildHandle
  3. JobSupport.invokeOnCompletion(
    onCancelling: Boolean,
    invokeImmediately: Boolean,
    handler: CompletionHandler
    )

划重点->通过CompletionHandler生成JobNode设置到父Job的state属性中,state属性有NodeList链表,用来保存回调列表

1处,如果当前只有一个CompletionHandler,则直接保存到state中

2处,如果当前有两个CompletionHandler则将state属性提升成链表

3处,如果当前大于2个CompletionHandler,则直接将该CompletionHandler添加到链表的尾部。

PS这块代码看起来比较复杂,写起来也比较费劲,初次接触肯定云里雾里,时间有限先写到这里吧。以后时间允许再详细写写

总而言之

1. Job通过state的NodeList对象与子Job建立关系,当Job被取消时通过这个链条向子Job发送取消请求

2. Job通过initParentJobInternal方法中获取到的parentHandle与父Job建立关系,当子Job被取消时通过该引用向父Job发送取消请求

3. 协程Job中的双向传播机制是协程框架算法决定的。当取消请求到达具体的Job时,Job如何处理则由Job实现类自己决定

4. 本文讲解了Job是如何建立关系的。下篇文章我将结合Job,SupervisorJob,coroutineScope,supervisorScope讲解他们在关系链路上如何处理双向cancel事件的

这篇关于抽丝剥茧聊Kotlin协程之协程是如何建立结构化并发的的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/372822

相关文章

Android kotlin语言实现删除文件的解决方案

《Androidkotlin语言实现删除文件的解决方案》:本文主要介绍Androidkotlin语言实现删除文件的解决方案,在项目开发过程中,尤其是需要跨平台协作的项目,那么删除用户指定的文件的... 目录一、前言二、适用环境三、模板内容1.权限申请2.Activity中的模板一、前言在项目开发过程中,尤

高并发环境中保持幂等性

在高并发环境中保持幂等性是一项重要的挑战。幂等性指的是无论操作执行多少次,其效果都是相同的。确保操作的幂等性可以避免重复执行带来的副作用。以下是一些保持幂等性的常用方法: 唯一标识符: 请求唯一标识:在每次请求中引入唯一标识符(如 UUID 或者生成的唯一 ID),在处理请求时,系统可以检查这个标识符是否已经处理过,如果是,则忽略重复请求。幂等键(Idempotency Key):客户端在每次

建立升序链表

题目1181:遍历链表 时间限制:1 秒 内存限制:32 兆 特殊判题:否 提交:2744 解决:1186 题目描述: 建立一个升序链表并遍历输出。 输入: 输入的每个案例中第一行包括1个整数:n(1<=n<=1000),接下来的一行包括n个整数。 输出: 可能有多组测试数据,对于每组数据, 将n个整数建立升序链表,之后遍历链表并输出。 样例输

Java并发编程之——BlockingQueue(队列)

一、什么是BlockingQueue BlockingQueue即阻塞队列,从阻塞这个词可以看出,在某些情况下对阻塞队列的访问可能会造成阻塞。被阻塞的情况主要有如下两种: 1. 当队列满了的时候进行入队列操作2. 当队列空了的时候进行出队列操作123 因此,当一个线程试图对一个已经满了的队列进行入队列操作时,它将会被阻塞,除非有另一个线程做了出队列操作;同样,当一个线程试图对一个空

java线程深度解析(五)——并发模型(生产者-消费者)

http://blog.csdn.net/Daybreak1209/article/details/51378055 三、生产者-消费者模式     在经典的多线程模式中,生产者-消费者为多线程间协作提供了良好的解决方案。基本原理是两类线程,即若干个生产者和若干个消费者,生产者负责提交用户请求任务(到内存缓冲区),消费者线程负责处理任务(从内存缓冲区中取任务进行处理),两类线程之

java线程深度解析(四)——并发模型(Master-Worker)

http://blog.csdn.net/daybreak1209/article/details/51372929 二、Master-worker ——分而治之      Master-worker常用的并行模式之一,核心思想是由两个进程协作工作,master负责接收和分配任务,worker负责处理任务,并把处理结果返回给Master进程,由Master进行汇总,返回给客

深入解析秒杀业务中的核心问题 —— 从并发控制到事务管理

深入解析秒杀业务中的核心问题 —— 从并发控制到事务管理 秒杀系统是应对高并发、高压力下的典型业务场景,涉及到并发控制、库存管理、事务管理等多个关键技术点。本文将深入剖析秒杀商品业务中常见的几个核心问题,包括 AOP 事务管理、同步锁机制、乐观锁、CAS 操作,以及用户限购策略。通过这些技术的结合,确保秒杀系统在高并发场景下的稳定性和一致性。 1. AOP 代理对象与事务管理 在秒杀商品

PostgreSQL中的多版本并发控制(MVCC)深入解析

引言 PostgreSQL作为一款强大的开源关系数据库管理系统,以其高性能、高可靠性和丰富的功能特性而广受欢迎。在并发控制方面,PostgreSQL采用了多版本并发控制(MVCC)机制,该机制为数据库提供了高效的数据访问和更新能力,同时保证了数据的一致性和隔离性。本文将深入解析PostgreSQL中的MVCC功能,探讨其工作原理、使用场景,并通过具体SQL示例来展示其在实际应用中的表现。 一、

使用协程实现高并发的I/O处理

文章目录 1. 协程简介1.1 什么是协程?1.2 协程的特点1.3 Python 中的协程 2. 协程的基本概念2.1 事件循环2.2 协程函数2.3 Future 对象 3. 使用协程实现高并发的 I/O 处理3.1 网络请求3.2 文件读写 4. 实际应用场景4.1 网络爬虫4.2 文件处理 5. 性能分析5.1 上下文切换开销5.2 I/O 等待时间 6. 最佳实践6.1 使用 as

Go并发模型:流水线模型

Go作为一个实用主义的编程语言,非常注重性能,在语言特性上天然支持并发,Go并发模型有多种模式,通过流水线模型系列文章,你会更好的使用Go的并发特性,提高的程序性能。 这篇文章主要介绍流水线模型的流水线概念,后面文章介绍流水线模型的FAN-IN和FAN-OUT,最后介绍下如何合理的关闭流水线的协程。 Golang的并发核心思路 Golang并发核心思路是关注数据流动。数据流动的过程交给cha