当前位置: 首页 > article >正文

Kotlin协程详解——协程上下文

目录

一、上下文结构

get()获取元素

minusKey()删除元素

fold()元素遍历

plus()添加元素

CombinedContext

Key

二、协程名称CoroutineName

三、上下文组合

四、协程作用域CoroutineScope

五、典型用例


协程的上下文,它包含用户定义的一些数据集合,这些数据与协程密切相关。它类似于map集合,可以通过key来获取不同类型的数据。同时CoroutineContext的灵活性很强,如果其需要改变只需使用当前的CoroutineContext来创建一个新的CoroutineContext即可。

在协程启动部分提到,启动协程需要三个部分,其中一个部分就是上下文,其接口类型是CoroutineContext,通常所见的上下文类型是CombinedContext或者EmptyCoroutineContext,一个表示上下文组合,另一个表示空。

协程上下文是Kotlin协程的基本结构单元,主要承载着资源获取,配置管理等工作,是执行环境的通用数据资源的统一管理者。除此之外,也包括携带参数,拦截协程执行等,是实现正确的线程行为、生命周期、异常以及调试的关键。

协程使用以下几种元素集定义协程行为,他们均继承自CoroutineContext:

  1. 【Job】:协程的句柄,对协程的控制和管理生命周期。
  2. 【CoroutineName】:协程的名称,用于调试
  3. 【CoroutineDispatcher】:调度器,确定协程在指定的线程执行
  4. 【CoroutineExceptionHandler】:协程异常处理器,处理未捕获的异常

简而言之,协程上下文是协程必备组成部分,管理了协程的线程绑定、生命周期、异常处理和调试。

一、上下文结构

看一下CoroutineContext的接口定义:

每一个CoroutineContext都有它唯一的一个Key其中的类型是Element,我们可以通过对应的Key来获取对应的具体对象。说的有点抽象我们直接通过例子来了解。

var context = Job() + Dispatchers.IO + CoroutineName("aa")
LogUtils.d("$context, ${context[CoroutineName]}")
context = context.minusKey(Job)
LogUtils.d("$context")
// 输出
[JobImpl{Active}@158b42c, CoroutineName(aa), LimitingDispatcher@aeb0f27[dispatcher = DefaultDispatcher]], CoroutineName(aa)
[CoroutineName(aa), LimitingDispatcher@aeb0f27[dispatcher = DefaultDispatcher]]

Element:协程上下文的一个元素,本身就是一个单例上下文,里面有一个key,是这个元素的索引。

JobDispatchersCoroutineName都实现了Element接口。如果需要结合不同的CoroutineContext可以直接通过+拼接,本质就是使用了plus方法。

可知,Element本身也实现了CoroutineContext接口。

这里我们再看一下官方解释:

/**
Persistent context for the coroutine. It is an indexed set of [Element] instances.
An indexed set is a mix between a set and a map.
Every element in this set has a unique [Key].*/

从官方解释可知,CoroutineContext是一个Element的集合,这种集合被称为indexed set,介于set 和 map 之间的一种结构。set 意味着其中的元素有唯一性,map 意味着每个元素都对应一个键。

如果将协程上下文内部的一系列上下文称为子上下文,上下文为每个子上下文分配了一个Key,它是一个带有类型信息的接口。

这个接口通常被实现为companion object。

源码中定义的子上下文,都会在内部声明一个静态的Key,类内部的静态变量意味着被所有类实例共享,即全局唯一的 Key 实例可以对应多个子上下文实例。

在一个类似 map 的结构中,每个键必须是唯一的,因为对相同的键 put 两次值,新值会代替旧值。通过上述方式,通过键的唯一性保证了上下文中的所有子上下文实例都是唯一的。

我们按照这个格式仿写一下然后反编译。

对比kt和Java文件,可以看到Key就是一个静态变量,且其实现类未做处理,作用与HashMap中的Key类似。

Key是静态变量,全局唯一,为Element提供唯一性保障。

前述内容总结如下:

  1. 协程上下文是一个元素的集合,单个元素本身也是一个上下文,其定义是递归的,自己包含若干个自己。
  2. 协程上下文这个集合有点像 set 结构,其中的元素都是唯一的,不重复的。其通过给每一个元素配有一个静态的键实例,构成一组键值对的方式实现。这使其类似 map 结构。这种介于 set 和 map 之间的结构称为indexed set。

get()获取元素

关于CoroutineContext,我们先看一下其是如何取元素的。

这里看一下Element、CombinedContext、EmptyCoroutineContext的内部实现,其中CombinedContext就是CoroutineContext集合结构的实现,EmptyCoroutineContext就表示一个空的CoroutineContext,它里面是空实现。

通过Key检索Element,返回值只能是Element或null,链表节点中的元素值,其中CombinedContext利用while循环实现了类似递归的效果,其中较早被遍历到的元素自然具有较高的优先级。

minusKey()删除元素

同理看一下Element、CombinedContext、EmptyCoroutineContext的内部实现。

internal class CombinedContext(//左上下文private val left: CoroutineContext,//右元素private val element: Element
) : CoroutineContext, Serializable {public override fun minusKey(key: Key<*>): CoroutineContext {//如果element就是要删除的元素,返回left,否则说明要删除的元素在left中,继续从left中删除对应的元素element[key]?.let { return left }//在左上下文中去掉对应元素val newLeft = left.minusKey(key)return when {//如果left中不存在要删除的元素,那么当前CombinedContext就不存在要删除的元素,直接返回当前CombinedContext实例newLeft === left -> this//如果left中存在要删除的元素,删除了这个元素后,left变为了空,那么直接返回当前CombinedContext的element就行newLeft === EmptyCoroutineContext -> element//如果left中存在要删除的元素,删除了这个元素后,left不为空,那么组合一个新的CombinedContext返回else -> CombinedContext(newLeft, element)}}......
}public object EmptyCoroutineContext : CoroutineContext, Serializable {public override fun minusKey(key: Key<*>): CoroutineContext = this......
}public interface Element : CoroutineContext {//如果key和自己的key匹配,那么自己就是要删除的Element,返回EmptyCoroutineContext(表示删除了自己),否则说明自己不需要被删除,返回自己public override fun minusKey(key: Key<*>): CoroutineContext =if (this.key == key) EmptyCoroutineContext else this......
}

如果把CombinedContext和Element结合来看,那么CombinedContext的整体结构如下:

其结构类似链表,left就是指向下一个结点的指针,get、minusKey操作大体逻辑都是先访问当前element,不满足,再访问left的element,顺序都是从right到left。

fold()元素遍历

fold也是递归的形式操作,fold的操作大体逻辑是:先访问left,直到递归到最后的element,然后再从left到right的返回,从而访问了所有的element。

plus()添加元素

关于CoroutineContext的元素添加方法,直接看其plus()实现,也是唯一没有被重写的方法。

public operator fun plus(context: CoroutineContext): CoroutineContext =
//如果要相加的CoroutineContext为空,那么不做任何处理,直接返回
if (context === EmptyCoroutineContext) this else
//如果要相加的CoroutineContext不为空,那么对它进行fold操作,可以把acc理解成+号左边的CoroutineContext,element理解成+号右边的CoroutineContext的某一个element
context.fold(this) { acc, element ->//首先从左边CoroutineContext中删除右边的这个elementval removed = acc.minusKey(element.key)//如果removed为空,说明左边CoroutineContext删除了和element相同的元素后为空,那么返回右边的element即可if (removed === EmptyCoroutineContext) element else {//如果removed不为空,说明左边CoroutineContext删除了和element相同的元素后还有其他元素,那么构造一个新的CombinedContext返回val interceptor = removed[ContinuationInterceptor]if (interceptor == null) CombinedContext(removed, element) else {val left = removed.minusKey(ContinuationInterceptor)if (left === EmptyCoroutineContext) CombinedContext(element, interceptor) elseCombinedContext(CombinedContext(left, element), interceptor)}}}

plus方法大部分情况下返回一个CombinedContext,即我们把两个CoroutineContext相加后,返回一个CombinedContext,在组合成CombinedContext时,+号右边的CoroutineContext中的元素会覆盖+号左边的CoroutineContext中的含有相同key的元素。plus的实现逻辑是将两个拼接的CoroutineContext封装到CombinedContext中组成一个拼接链,同时每次都将ContinuationInterceptor添加到拼接链的最尾部.

这个覆盖操作就在fold方法的参数operation代码块中完成,通过minusKey方法删除掉重复元素。

plus方法中可以看到里面有个对ContinuationInterceptor的处理,目的是让ContinuationInterceptor在每次相加后都能变成CoroutineContext中的最后一个元素。

ContinuationInterceptor继承自Element,称为协程上下文拦截器,作用是在协程执行前拦截它,从而在协程执行前做出一些其他的操作。通过把ContinuationInterceptor放在最后面,协程在查找上下文的element时,总能最快找到拦截器,避免了递归查找,从而让拦截行为前置执行。

CombinedContext

internal class CombinedContext(private val left: CoroutineContext,private val element: Element
) : CoroutineContext, Serializable {override fun <E : Element> get(key: Key<E>): E? {var cur = thiswhile (true) {cur.element[key]?.let { return it }val next = cur.leftif (next is CombinedContext) {cur = next} else {return next[key]}}}...
}

注意看它的两个参数,我们直接拿上面的例子来分析

Job() + Dispatchers.IO
(Job, Dispatchers.IO)

Job对应于leftDispatchers.IO对应element。如果再拼接一层CoroutineName(aa)就是这样的

((Job, Dispatchers.IO),CoroutineName)

功能类似与链表,但不同的是你能够拿到上一个与你相连的整体内容。与之对应的就是minusKey方法,从集合中移除对应KeyCoroutineContext实例。

有了这个基础,我们再看它的get方法就很清晰了。先从element中去取,没有再从之前的left中取。

Key

那么这个Key到底是什么呢?我们来看下CoroutineName

public data class CoroutineName(/*** User-defined coroutine name.*/val name: String
) : AbstractCoroutineContextElement(CoroutineName) {/*** Key for [CoroutineName] instance in the coroutine context.*/public companion object Key : CoroutineContext.Key<CoroutineName>/*** Returns a string representation of the object.*/override fun toString(): String = "CoroutineName($name)"
}

很简单它的Key就是CoroutineContext.Key<CoroutineName>,当然这样还不够,需要继续结合对于的operator get方法,所以我们再来看下Elementget方法

public override operator fun <E : Element> get(key: Key<E>): E? =@Suppress("UNCHECKED_CAST")if (this.key == key) this as E else null

这里使用到了Kotlinoperator操作符重载的特性。那么下面的代码就是等效的。

context.get(CoroutineName)
context[CoroutineName]

所以我们就可以直接通过类似于Map的方式来获取整个协程中CoroutineContext集合中对应KeyCoroutineContext实例。

二、协程名称CoroutineName

CoroutineName是用户用来指定的协程名称的,用于方便调试和定位问题。


协程内部可以通过coroutineContext这个全局属性直接获取当前协程的上下文。

三、上下文组合

如果要传递多个上下文元素,CoroutineContext可以使用"+"运算符进行合并。由于CoroutineContext是由一组元素组成的,所以加号右侧的元素会覆盖加号左侧的元素,进而组成新创建的CoroutineContext。

如果有重复的元素(key一致)则右边的会代替左边的元素,相关原理参看协程上下文结构章节。

四、协程作用域CoroutineScope

CoroutineScope实际上是一个CoroutineContext的封装,当我们需要启动一个协程时,会在CoroutineScope的实例上调用构建函数,如async和launch。

在构建函数中,一共出现了3个CoroutineContext。

查看协程构建函数async和launch的源码,其第一行都是如下代码:

进一步查看:

构建器内部进行了一个CoroutineContext拼接操作,plus左值是CoroutineScope内部的CoroutineContext,右值是作为构建函数参数的CoroutineContext。

抽象类AbstractCoroutineScope实现了CoroutineScope和Job接口。大部分CoroutineScope的实现都继承自AbstractCoroutineScope,意味着他们同时也是一个Job。

从上述分析可知:coroutine context = parent context + coroutine job

五、典型用例

全限定Context
launch( Dispatchers.Main + Job() + CoroutineName("HelloCoroutine") + CoroutineExceptionHandler { _, _ -> /* ... */ }) {
/* ... */
}

全限定Context,即全部显式指定具体值的Elements。不论你用哪一个CoroutineScope构建该协程,它都具有一致的表现,不会受到CoroutineScope任何影响。

CoroutineScope Context

基于Activity生命周期实现一个CoroutineScope

Dispatcher:使用Dispatcher.Main,以在UI线程进行绘制
Job:在onCreate时构建,在onDestroy时销毁,所有基于该CoroutineContext创建的协程,都会在Activity销毁时取消,从而避免Activity泄露的问题
临时指定参数

CoroutineContext的参数主要有两个来源:从scope中继承+参数指定。我们可以用withContext便捷地指定某个参数启动子协程,例如我们想要在协程内部执行一个无法被取消的子协程:

读取协程上下文参数

通过顶级挂起只读属性coroutineContext获取协程上下文参数,它位于 kotlin-stdlib / kotlin.coroutines / coroutineContext

Nested Context内嵌上下文

内嵌上下文切换:在协程A内部构建协程B时,B会自动继承A的Dispatcher。

可以在调用async时加入Dispatcher参数,切换到工作线程

推荐文章

https://zhuanlan.zhihu.com/p/552225674

Kotlin协程实现原理:Suspend&CoroutineContext

相关文章:

Kotlin协程详解——协程上下文

目录 一、上下文结构 get()获取元素 minusKey()删除元素 fold()元素遍历 plus()添加元素 CombinedContext Key 二、协程名称CoroutineName 三、上下文组合 四、协程作用域CoroutineScope 五、典型用例 协程的上下文&#xff0c;它包含用户定义的一些数据集合&#x…...

手写一个C++ Android Binder服务及源码分析

手写一个C Android Binder服务及源码分析 前言一、 基于C语言编写Android Binder跨进程通信Demo总结及改进二、C语言编写自己的Binder服务Demo1. binder服务demo功能介绍2. binder服务demo代码结构图3. binder服务demo代码实现3.1 IHelloService.h代码实现3.2 BnHelloService.c…...

今日AI和商界事件(2025-02-10)

今日AI领域的相关事件包括&#xff1a; 一、技术与应用进展 全球首例AI驱动供应链攻击曝光&#xff1a; 网络安全机构披露一起新型供应链攻击事件&#xff0c;攻击者利用AI技术生成高度仿真的供应商邮件&#xff0c;诱骗目标企业员工下载恶意软件&#xff0c;进而渗透至大众汽…...

全面理解-c++中的异常处理机制

C 的异常处理机制是一种用于处理程序运行时错误的结构化方法&#xff0c;通过分离正常逻辑与错误处理代码&#xff0c;提高代码的可读性和可维护性。以下是其核心组成部分和工作原理的详细说明&#xff1a; 1. 异常处理的三大关键字 1.1 try 块 作用&#xff1a;包裹可能抛出异…...

Deep Dive into LLMs like ChatGPT - by Andrej Karpathy

https://www.youtube.com/watch?v7xTGNNLPyMIhttps://www.youtube.com/watch?v7xTGNNLPyMIDeep Dive into LLMs like ChatGPT - by Andrej Karpathy_哔哩哔哩_bilibilihttps://www.youtube.com/watch?v7xTGNNLPyMI转载自Andrej Karpathy Youtube ChannelThis is a general a…...

react实例与总结(一)

目录 一、简单认识 1.1、特点 1.2、JSX语法规则 1.3、函数组件和类式组件 1.4、类组件三大属性state、props、refs 1.4.1、state 1.4.2、props 1.4.3、refs 1.5、事件处理 1.6、收集表单数据—非受控组件和受控组件 1.7、高阶函数—函数柯里化 1.8、生命周期—新旧…...

51单片机(国信长天)矩阵键盘的基本操作

在CT107D单片机综合训练平台上&#xff0c;首先将J5处的跳帽接到1~2引脚&#xff0c;使按键S4~S19按键组成4X4的矩阵键盘。在扫描按键的过程中&#xff0c;发现有按键触发信号后(不做去抖动)&#xff0c;待按键松开后&#xff0c;在数码管的第一位显示相应的数字:从左至右&…...

在cursor/vscode中使用godot C#进行游戏开发

要在 Visual Studio Code(VS Code)中启动 C#Godot 项目&#xff0c;可以按照以下步骤进行配置&#xff1a; 1.安装必要的工具 • 安装 Visual Studio Code&#xff1a;确保你已经安装了最新版本的 VS Code。 • 安装.NET SDK&#xff1a;下载并安装.NET 7.x SDK&#xff08;…...

机器学习怎么学习,还有算法基本的源代码

1.scikit-learn官方文档&#xff0c;中文版/英文版 中文社区&#xff1a;https://scikit-learn.org.cn/ 中文官方文档&#xff1a;https://scikitlearn.com.cn/ 英文版&#xff1a;https://scikit-learn.org/stable/&#xff08;翻墙&#xff09; 2.菜鸟教程&#xff1a;AI&a…...

STM32 RTC亚秒

rtc时钟功能实现&#xff1a;rtc模块在stm32内部&#xff0c;由电池或者主电源供电。如下图&#xff0c;需注意实现时仅需设置一次初始化。 1、stm32cubemx 代码生成界面设置&#xff0c;仅需开启时钟源和激活日历功能。 2、生成的代码,需要对时钟进行初始化&#xff0c;仅需…...

【Linux】深入理解linux权限

&#x1f31f;&#x1f31f;作者主页&#xff1a;ephemerals__ &#x1f31f;&#x1f31f;所属专栏&#xff1a;Linux 目录 前言 一、权限是什么 二、用户和身份角色 三、文件属性 1. 文件属性表示 2. 文件类型 3. 文件的权限属性 四、修改文件的权限属性和角色 1. …...

json格式,curl命令,及轻量化处理工具

一. JSON格式 JSON&#xff08;JavaScript Object Notation&#xff09; 是一种轻量级的数据交换格式。它基于一个子集的JavaScript编程语言&#xff0c;使用人类易于阅读的文本格式来存储和表示数据。尽管名字中有“JavaScript”&#xff0c;但JSON是语言无关的&#xff0c;几…...

DeepSeek模拟阿里面试——java面向对象

作为一位阿里高级Java程序员面试官&#xff0c;我会围绕Java面向对象编程的核心概念、实际应用以及设计原则设计问题&#xff0c;以全面评估候选人的理解和应用能力。以下是可能的面试问题&#xff1a; 基本概念与实现方式 请解释Java中封装、继承、多态的基本概念及其在Java中…...

web直播弹幕抓取分析 signature

声明: 本文章中所有内容仅供学习交流使用&#xff0c;不用于其他任何目的&#xff0c;抓包内容、敏感网址、数据接口等均已做脱敏处理&#xff0c;严禁用于商业用途和非法用途&#xff0c;否则由此产生的一切后果均与作者无关&#xff01; 前言 最近遇到太多难点了卡了很久&am…...

【04】RUST特性

文章目录 隐藏shadowing所有权ownership堆区&栈区所有权规则变量&数据Copy Trait与Drop TraitCopy TraitDrop Trait移动克隆函数参数与返回值的所有权参数引用可变引用悬垂引用slice生命周期隐藏shadowing 有点像同名覆盖 let mut guess = String::new();let guess: u3…...

PL/SQL块结构

目录 一、声明部分&#xff08;declare&#xff09; 二、执行部分&#xff08;begin end&#xff09; 三、异常处理部分 &#xff08;Exception end&#xff09; 四、代码示例 PL/SQL&#xff08;Procedural Language/Structured Query Language&#xff09;是Oracle数据库…...

基于 FFmpeg 和 OpenGLES 的 iOS 视频预览和录制技术方案设计

基于 FFmpeg 和 OpenGLES 的 iOS 视频预览和录制技术方案设计 在 iOS 上实现一个基于 FFmpeg 和 OpenGLES 的视频预览和录制功能,需要结合 FFmpeg 的强大音视频处理能力和 OpenGLES 的高效图形渲染能力。以下是一个完整的技术方案设计,包含项目的架构设计、模块划分、技术选…...

【LeetCode 刷题】贪心算法(4)-区间问题

此博客为《代码随想录》贪心算法章节的学习笔记&#xff0c;主要内容为贪心算法区间问题的相关题目解析。 文章目录 55. 跳跃游戏45. 跳跃游戏 II452. 用最少数量的箭引爆气球435. 无重叠区间763. 划分字母区间56. 合并区间 55. 跳跃游戏 题目链接 class Solution:def canJu…...

提示工程 | 目的 | 常用技巧

什么是提示工程 提示工程也叫指令工程&#xff0c;Prompt就是你发给大模型的指令&#xff0c;比如&#xff1a;画幅画&#xff0c;写首诗等。貌似简单&#xff0c;但意义非凡&#xff0c;Prompt是AGI时代的编程语言&#xff0c;Prompt工程是AGI时代的软件工程&#xff0c;提示…...

ABP框架9——自定义拦截器的实现与使用

一、AOP编程 AOP定义:面向切片编程&#xff0c;着重强调功能&#xff0c;将功能从业务逻辑分离出来。AOP使用场景&#xff1a;处理通用的、与业务逻辑无关的功能&#xff08;如日志记录、性能监控、事务管理等&#xff09;拦截器:拦截方法调用并添加额外的行为&#xff0c;比如…...

Generate html

"Generate HTML"&#xff08;生成 HTML&#xff09;指的是通过程序或工具自动创建 HTML 代码的过程。HTML&#xff08;超文本标记语言&#xff09;是用于创建网页内容和结构的标准语言。生成 HTML 通常意味着通过某些方式自动化地构建或生成网页的结构和元素&#xf…...

CUDA 计算平台 CUDA 兼容性【笔记】

在 b 站看过的两个关于 CUDA 的技术分享&#xff0c;整理分享下对自己有用的课件。 20231130 2023第9期 聊一聊常见的AI计算平台库_哔哩哔哩_bilibili20230831 2023第6期 聊一聊CUDA兼容性_哔哩哔哩_bilibili 文章目录 CUDA 计算平台CUDA 函数库介绍英伟达三大护城河&#xff1…...

移动(新)魔百盒刷机教程[M301A_YS]

刚刚成功刷了一个坏的魔百盒&#xff0c;简单记录一下。 刷电视盒子有两种&#xff1a;卡刷和线刷。 线刷 一、线刷准备 1.刷机工具 Amlogic USB Burning Tool 晶晨线刷烧录工具 2.固件 根据盒子的型号、代工等找到对应的固件 二、线刷步骤 电脑打开下好的 Amlogic US…...

最新消息 | 德思特荣获中国创新创业大赛暨广州科技创新创业大赛三等奖!

2024年12月30日&#xff0c;广州市科技局公开第十三届中国创新创业大赛&#xff08;广东广州赛区&#xff09;暨2024年广州科技创新创业大赛决赛成绩及拟获奖企业名单&#xff0c;德思特获得了智能与新能源汽车初创组【第六名】【三等奖】的好成绩&#xff01; 关于德思特&…...

基于机器学习的DDoS检测系统实战

基于机器学习的DDoS检测系统实战&#xff08;PythonScikit-learn&#xff09;&#xff5c;毕业设计必备 摘要&#xff1a;本文手把手教你从0到1实现一个轻量级DDoS攻击检测系统&#xff0c;涵盖数据预处理、特征工程、模型训练与可视化分析。 一、项目背景与意义 DDoS&#x…...

ubuntu安装VMware报错/dev/vmmon加载失败

ubuntu安装VMware报错/dev/vmmon加载失败&#xff0c;解决步骤如下&#xff1a; step1&#xff1a;为vmmon和vmnet组件生成密钥对 openssl req -new -x509 -newkey rsa:2048 -keyout VMW.priv -outform DER -out VMW.der -nodes -days 36500 -subj "/CNVMware/"ste…...

使用条件随机场(CRF)进行文本分类并评估模型性能

目标&#xff1a; 使用条件随机场&#xff08;CRF&#xff09;模型对文本数据进行分类&#xff0c;并评估模型的性能。任务包括读取数据、划分训练集和测试集、训练CR # 1.数据读取与预处理&#xff1a; # o使用open函数读取包含文本和标签的CSV文件。 # o将每一行数据分为文本…...

python的列表、元组、深拷贝、浅拷贝(四)

python的列表 一、序列1. 序列定义2. 序列数据类型包括3.特点&#xff1a;都支持下面的特性 二、 列表1. 列表的创建2. 列表的基本特性(1) 连接操作符喝重复操作符(2) 成员操作符&#xff08;in , not in &#xff09;(3) 索引(4) 切片练习(5) for循环 3. 列表的常用方法(1) 一…...

2.10作业

思维导图 C C语言...

【深度学习】多目标融合算法(四):多门混合专家网络MMOE(Multi-gate Mixture-of-Experts)

目录 一、引言 二、MMoE&#xff08;Multi-gate Mixture-of-Experts&#xff0c;多门混合专家网络&#xff09; 2.1 技术原理 2.2 技术优缺点 2.3 业务代码实践 2.3.1 业务场景与建模 2.3.2 模型代码实现 2.3.3 模型训练与推理测试 2.3.4 打印模型结构 三、总结 一、…...