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

跟我学c++中级篇——再谈C++20中的协程

一、协程

在前面分析过协程是什么,也对c++20中的协程的应用进行了举例说明。所以这次重点分析一下c++20中的整体构成及应用的方式。等明白了协程是如何动作的,各种情况下如下何处理相关的事件,那么在以后写协程时就不会死搬硬套了。

二、整体说明

在C++20中的协程中,首先要明白几个主要的类型和关键字。在协程中可以划分为三种类型以及一种状态,这三种类型(或者说表现出来的数据结构)是:
1、promise(promise_type)
这个在C++11中就有,此处和其意义相似,协程内部通过此对象来获取结果及异常。协程中要求返回对象必须是promis_type,这是一个接口类型。它一般要实现:

get_return_object: 获取协程的返回对象
initial_suspend:协程初始化的时挂起
final_suspend:协程结束时挂起的操作

initial_suspend和final_suspend 有点象钩子函数,也有点像JAVA的Spring中的拦截器,前者在执行协程(函数)前运行,后者在协程返回前执行。这里需要注意的是,如果final_suspend 运行了协程挂起的动作(比如使用了std::suspend_always{}),就需要记得在协程结束后手动destroy来销毁,否则的话,协程将自动销毁。这个和线程的detach和join有点类似。
另外它还有其它一些方法:

void return_void(): 如果没有实现co_return或单纯调用co_return以及co_return expr中expr返回值为void,必须有,否则报错
void return_value():co_return expr中expr返回值为非void
void yield_value():如果程序中调用了co_yield,则必须有
unhandled_exception(): 异常控制

同时,promise_type还会管理协程的相关栈空间,即一些临时变量、参数、返回值及一些寄存器的上下文等。

2、awaitable(awaiter)
awaitable和awaiter是比较让初学者有些感到迷惑的,有人可能觉得为什么要有两个类似的对象。其实这个非常好理解,前者是用来处理co_wait的expr的对象,后者是具体的处理对象或者等待器。而awaitable可以理解为居中的一层抽象虽然co_wait awaiter,但直接使用awaiter便不再依赖于抽象而是依赖于具体实现。所以便把awaiter抽象出来,具体由awaitable来实现。所以expr类型需要是一个awaitable类型。而awaitable可以转换成awaiter。
它其中需要实现的接口包括:

await_ready():是否挂起协程。返回 true,co_await不挂起函数,否则挂起(一般异步都是false)并调用await_suspend
await_resume():co_await expt的返回值,一般为空(如果有返回值则 auto ret = co_await expt)
await_suspend(handle):协程挂起时的行为,可能通过handle得到当前协程的状态,以此来处理挂起时的动作

在c++20的协程说明中有详细的说明哪几种情况可以转换成awaitable。如果没有什么复杂的动作,可以使用在标准库里实现的两个此类型的结构体:

struct suspend_never {_NODISCARD constexpr bool await_ready() const noexcept {return true;}constexpr void await_suspend(coroutine_handle<>) const noexcept {}constexpr void await_resume() const noexcept {}
};struct suspend_always {_NODISCARD constexpr bool await_ready() const noexcept {return false;}constexpr void await_suspend(coroutine_handle<>) const noexcept {}constexpr void await_resume() const noexcept {}
};

代码很简单,注意noexcept,必须有。
这里重点需要说明一下await_suspend,其实异步处理最主要的就是在此处,它可以通过传入的外部包裹的协程handle来管理协程动作。所以在网上或者资料上的一些例程在此处直接调用resume,其实是没有展现出异步的作用。
另外在await_suspend中的参数如果有操作promise_type则可以写成std::coroutine_handle<>,这就是默认(void)或自动推导就可以了。

3、coroutine_handle
这个就是刚刚提到的std::coroutine_handle<>,它一般常用的有两个方法:

handle.resume():协程恢复执行
handle.destroy():销毁协程(在前文提到的final_suspend挂起时)

当然还done,from_address等接口,大家可以参考相关资料文档。
需要注意的是,这个句柄类似于浅拷贝,析构函数不处理相关的协程内部状态的内存,需要操作destroy函数,但大多数情况下都是自动销毁,只有在前面提到的那种情况下,需要手动调用destroy函数。同样,销毁掉句柄后,被复制的句柄成为了类似于悬垂指针的存在。

而一种状态是协程状态coroutine state,它主要有包含以下几点:
1、the promise object
2、the parameters (all copied by value)
3、some representation of the current suspension point, so that a resume knows where to continue, and a destroy knows what local variables were in scope(这个有点类似于线程上下文的意思)
4、local variables and temporaries whose lifetime spans the current suspension point.
协程的状态有点类似于线程的上下文,这个需要大家自己去体会。

在协程中主要有三个关键字co_wait,co_yield和co_return:

1、co_wait
它是一个一元运算符,用来处理协程的暂停并将控制权返回给调用者直到重新恢复协程,其操作的是一个表达式,即co_wati expr(即awaiter) 。和上面的awaitable对应。
2、co_yield
这个相当于暂停协程并返回一个值,而这个值可以被重新利用即前面提到的yield_value函数。
3、co_return
这个好理解,它直接就返回值并结束协程。

这样把三种类型的概念和协程的状态以及三个关键字的关系搞清楚,是不是协程的用法已经呼之欲出,小的细节可以在实际应用中踩踩坑并多看文档来处理。

三、协程的执行流程

看完上面的分析,下面再把它们的关系用流程串边一下,即协程启动后,它们之间如何配合工作的:
1、首先分配内存并初始化协程状态
2、相关参数复制到协程
3、构造promise对象
4、通过调用 promise.get_return_object()并保存结果供协程首次挂起返回值给调用者
5、执行co_await promise.initial_suspend(),如果无复杂需求可使用STL中定义的 std::suspend_always (始终挂起)和std::suspend_never(永不挂起)
6.1 协程函数执行至 co_return expr语句:
若 expr 为 void 则执行 promise.return_void(),否则执行 promise.return_value(expr)
按照创建顺序的倒序销毁局部变量和临时变量
执行 co_await promise.final_suspend()

6.2 当协程执行到 co_yield expr语句:
调用co_await promise.yield_value(expr)

6.3 当协程执行到 co_await expr语句:
通过 expr 获得 awaiter 对象。
执行 awaiter.await_ready(),若为 true 则直接返回awaiter.await_resume(),否则将协程挂起并保存状态,执行 awaiter.await_suspend(),若其返回值为 void 或者 true 则成功挂起,将控制权返还给调用者,直到 handle.resume() 执行后该协程才会恢复执行,将 awaiter.await_resume() 作为表达式的返回值

6.4 当协程因为某个未捕获的异常导致终止:
捕获异常并调用 promise.unhandled_exception()
调用 co_await promise.final_suspend() 并 co_await 它的结果(例如,恢复某个继续或发布某个结果)。此时开始恢复协程是未定义行为。

6.5 当协程状态销毁时(通过协程句柄主动销毁 / co_return 返回 / 未捕获异常):
调用promise 对象的析构函数
调用传入参数的析构函数
释放协程状态占用内存
转移执行回调用方/恢复方

以上这些在官方文档中有更详细的说明,大家可以在实际开发中去查阅相关信息。
是不是有点复杂,是有点复杂啊。这个样子还是不能让普通程序员用得自由,也就是没有解决简单的问题,所以协程还需要继续努力(网上有各种基于此的协程库)。

四、例程

这里的例程重点是对上面的分析的一种代码的说明,请仔细看代码并和上面的分析说明对照,会有更深的理解,先看一个例子:

#include <coroutine>
#include <iostream>
#include <stdexcept>
#include <thread>auto switch_to_new_thread(std::jthread& out)
{struct awaitable{std::jthread* p_out;bool await_ready() { return false; }void await_suspend(std::coroutine_handle<> h){std::jthread& out = *p_out;if (out.joinable())throw std::runtime_error("jthread 输出参数非空");out = std::jthread([h] { h.resume(); });// 潜在的未定义行为:访问潜在被销毁的 *this// std::cout << "新线程 ID:" << p_out->get_id() << '\n';std::cout << "新线程 ID:" << out.get_id() << '\n'; // 这样没问题}void await_resume() {}};return awaitable{&out};
}struct task
{struct promise_type{task get_return_object() { return {}; }std::suspend_never initial_suspend() { return {}; }std::suspend_never final_suspend() noexcept { return {}; }void return_void() {} //和上文中的说明对照void unhandled_exception() {}};
};task resuming_on_new_thread(std::jthread& out)
{std::cout << "协程开始,线程 ID:" << std::this_thread::get_id() << '\n';co_await switch_to_new_thread(out);// 等待器在此销毁std::cout << "协程恢复,线程 ID:" << std::this_thread::get_id() << '\n';
}int main()
{std::jthread out;resuming_on_new_thread(out);
}

可能的输出:

协程开始,线程 ID:139972277602112
新线程 ID:139972267284224
协程恢复,线程 ID:139972267284224

怎么样,协程进行了线程的自动调度,方便很多吧。但是这种在不同线程间调度时需要注意,不能在另外一个线程内恢复协程否则容易出现数据处理的问题,这个就看应用的场景了。用不好,就直接挂了。这个例子中也有风险提示,大家看一下就明白了。
再看一个简单的例子:

#include <coroutine>
#include <iostream>struct MyCoroutine {struct MyPromise {MyCoroutine get_return_object() {return std::coroutine_handle<MyPromise>::from_promise(*this);}std::suspend_never initial_suspend() { return {}; }// 使用suspend_always,需要手动 destroy,noexcept必须auto final_suspend() noexcept{ return   std::suspend_always{}; }void unhandled_exception() {}void return_void() {} //返回空必须有};using promise_type = MyPromise;//尖括号中MyPromise类型可自动推导MyCoroutine(std::coroutine_handle<> h) : handle(h) {}std::coroutine_handle<MyPromise> handle;};MyCoroutine first() {std::cout << "this is my  first \n" ;co_await std::suspend_always{};std::cout << "coroutine!\n";
}int main() {MyCoroutine my = first();my.handle.resume();my.handle.destroy();//手动释放return 0;
}

看到上面的第一个例子中的风险,那么如何安全的调度协程实现await_suspend的异步操作情况,两种方法,一种是在主线程中写一个任务处理函数,不断的去探查协程挂起后的操作,并把await_suspend的恢复放到主线程中去。第二种方法就是使用和线程池类似的方式,将任务注册到队列,通过消息来驱动协程工作并最终将任务结果返回。
github上有阿里开源的协程库,应用起来会更简单(https://github.com/alibaba/async_simple)。
源码面前,了无秘密。

五、总结

协程最大特点是它可以跨越线程来进行操作,而在线程中一般数据处理要么在线程中独自控制要么需要加锁。所以协程应用起来更灵活,这也是为什么协程能更好的发挥线程的作用并同时呈现更好异步操作的原因。这个在GO的协程测试中已经有验证,协程可以开出几百万个,但线程一般到几百个就达到瓶颈了。
协程的开发会在大多数场景下替代线程的开发,只有一个最大原因,简单,功能强大倒是其次。希望c++中的协程在后面会变得更简单好用。

相关文章:

跟我学c++中级篇——再谈C++20中的协程

一、协程 在前面分析过协程是什么&#xff0c;也对c20中的协程的应用进行了举例说明。所以这次重点分析一下c20中的整体构成及应用的方式。等明白了协程是如何动作的&#xff0c;各种情况下如下何处理相关的事件&#xff0c;那么在以后写协程时就不会死搬硬套了。 二、整体说…...

【计算机毕业设计】SSM企业工资管理系统

项目介绍 本项目包含管理员与普通员工两种角色&#xff0c; 管理员角色包含以下功能&#xff1a; 管理员登录,员工管理,部门管理,岗位管理,职称管理,工龄奖金管理,工资项管理,考勤管理,工资查询,统计图表等功能。 员工角色包含以下功能&#xff1a; 员工登录,个人信息管理…...

x-cmd pkg | doggo - 现代化的 DNS 客户端

目录 简介首次用户快速实验指南功能特点类似工具与竞品进一步探索 简介 doggo 是一个由 Karan Sharma 于 2020 年使用 Go 语言开发的 DNS 客户端。它类似于 dig 命令&#xff0c;但旨在以现代化、简洁和可读的格式输出 DNS 查询结果。 首次用户快速实验指南 使用 x doggo 即可…...

c++-智能指针

1、概念 堆内存的对象需要手动使用delete销毁&#xff0c;如果忘记使用delete销毁就会造成内存泄漏。 所以C在ISO 98标注中引入了智能指针的概念&#xff0c;并在C11 中趋于完善。 使用智能指针可以让堆内存对象具有栈内存对象的特性。原理时给需要自动回收的堆内存对象套上一层…...

烟花燃放如何管控?智能分析网关V4烟火检测保障烟火安全

一、方案背景 随着元旦佳节的热潮退去&#xff0c;春节也即将来临&#xff0c;在众多传统的中国节日里&#xff0c;烟花与烧纸祭祀都是必不可少的&#xff0c;一方面表达了人们对节日的庆祝的期许&#xff0c;另一方面也是一种对故者思念的寄托。烟花爆竹的燃放不仅存在着巨大的…...

Vue实现版本号输入、删除时光标自动移动到上、下一个输入框前端demo

前言 首先声明&#xff0c;我平时的工作主要是后端JAVA开发&#xff0c;该demo为前端练习&#xff0c;记录一下劳动成果&#xff0c;希望对大家有所帮助&#xff0c;如果有写的不妥的地方&#xff0c;欢迎大家指正&#xff0c;一起学习、共同进步。 背景 手机验证码、银行卡…...

【胖虎的逆向之路】Android自制Https证书实现双向认证

Android自制Https证书实现双向认证 1.基本概念1.1 HTTP1.2 HTTPS1.3 加密方式1.3.1 对称加密1.3.2 非对称加密 1.4 SSL 功能1.4.1 客户对服务器的身份认证1.4.2 服务器对客户的身份认证1.4.3 建立服务器与客户之间安全的数据通道 1.5 CA 证书 2.证书生成2.1 生成根证书&#xf…...

解析千兆多模光模块SFP-GE-SX

千兆多模光模块是一种基于光纤通信的光电转换模块&#xff0c;具有千兆&#xff08;Gigabit&#xff09;级别的传输速率。本文将对千兆多模光模块的定义、传输距离、参数、及其应用领域等进行详细介绍。 一、千兆多模光模块SFP-GE-SX是什么&#xff1f; 千兆多模光模块SFP-GE-S…...

Go语言基础简单了解

文章目录 前言关于Go学习流程 基础语法注释变量常量数据类型运算符fmt库 流程控制if、switch、selectfor、break、continue遍历String 函数值传递和引用传递deferinit匿名、回调、闭包函数 数组和切片Map结构体自定义数据类型接口协程和channel线程锁异常处理泛型文件读取文件写…...

kafka重平衡经验总结

文章目录 概要背景解决方法技术细节小结 概要 关于kafka重平衡问题在实践工作的应用 背景 重平衡包括以下几种场景: 消费者组内成员发生变更&#xff0c;这个变更包括了增加和减少消费者。注意这里的减少有很大的可能是被动的&#xff0c;就是某个消费者崩溃退出了主题的分…...

Py之jupyter_client:jupyter_client的简介、安装、使用方法之详细攻略

Py之jupyter_client:jupyter_client的简介、安装、使用方法之详细攻略 目录 jupyter_client的简介 jupyter_client的安装 jupyter_client的使用方法 1、基础用法 (1)、获取内核信息 (2)、执行代码块 (3)、远程执行代码 jupyter_client的简介 jupyter_client 包含 Jupyter 协…...

61.网游逆向分析与插件开发-游戏增加自动化助手接口-游戏红字公告功能的逆向分析

内容来源于&#xff1a;易道云信息技术研究院VIP课 上一节内容&#xff1a;游戏公告功能的逆向分析与测试-CSDN博客 码云地址&#xff08;master分支&#xff09;&#xff1a;https://gitee.com/dye_your_fingers/sro_-ex.git 码云版本号&#xff1a;63e04cc40f649d10ba2f4f…...

neo4j查询语言Cypher详解(五)--apoc

APOC (Awesome Procedures on Cypher)是一个Neo4j库&#xff0c;它提供了对其他过程和函数的访问&#xff0c;扩展了Cypher查询语言的使用。 apoc MATCH (n:Movie) CALL apoc.create.addLabels( n, [ n.genre ] ) YIELD node REMOVE node.genre RETURN node;附录 参考 apoc…...

odoo17 | 视图操作按钮

前言 到目前为止&#xff0c;我们主要通过声明字段和视图来构建我们的模块。在上一章中&#xff0c;我们刚刚通过计算字段和onchanges引入了业务逻辑。在任何真实的业务场景中&#xff0c;我们都会希望将一些业务逻辑链接到操作按钮。在我们的房地产示例中&#xff0c;我们希望…...

KBDPL.DLL文件丢失,软件游戏无法启动,修复方法

不少小伙伴&#xff0c;求助说遇到Windows弹窗提示“KBDPL.DLL文件丢失&#xff0c;应用无法启动的问题”&#xff0c;不知道应该怎么修复&#xff1f; 首先&#xff0c;先来了解“KBDPL.DLL文件”是什么&#xff1f; kbdpl.dll是Windows操作系统的一部分&#xff0c;是一个动…...

Webpack5 常用优化总结

本文主要总结经常用到的一些代码性能优化、减小代码体积、提升webpack打包构建速度等内容的方法。具体的实现可参考webpack官网查看相关示例。 注&#xff1a;如果读者还未接触过webpack&#xff0c;请先了解webpack的基本使用。 正文&#xff1a; SourceMap ---- 提升开发体…...

Oracle-视图与索引

视图 简介 视图是一种虚表 视图建立在已有表的基础上&#xff0c;视图赖以建立的的这些表成为基表 向视图提供的数据的内容的语句的select 语句&#xff0c;可以将视图理解为存储起来的select 语句 视图向用户提供基表数据的另外一种表现形式 视图的好处 控制数据访问 …...

在Linux写自己的第一个程序“hello Linux”

01.nano指令 我们在Windows中有很多的编译环境&#xff0c;大家应该都很熟悉&#xff0c;但是在Linux中&#xff0c;我们怎么写代码呢&#xff1f; 这里&#xff0c;我介绍一个非常简单的指令->nano 这个指令就类似于我们Windows中的记事本&#xff0c;使用方法也很简单 …...

【AI视野·今日Robot 机器人论文速览 第六十八期】Tue, 2 Jan 2024

AI视野今日CS.Robotics 机器人学论文速览 Tue, 2 Jan 2024 Totally 12 papers &#x1f449;上期速览✈更多精彩请移步主页 Daily Robotics Papers Edge Computing based Human-Robot Cognitive Fusion: A Medical Case Study in the Autism Spectrum Disorder Therapy Author…...

图像识别快速实现

文本的跑通了&#xff0c;接下来玩玩图片场景 1. 引入模型 再另起类test_qdrant_img.py&#xff0c;转化图片用到的模型和文本不太一样&#xff0c;我们这里使用ResNet-50模型 import unittest from qdrant_client.http.models import Distance, VectorParams from qdrant_cl…...

一文详解动态 Schema

在数据库中&#xff0c;Schema 常有&#xff0c;而动态 Schema 不常有。 例如&#xff0c;SQL 数据库有预定义的 Schema&#xff0c;但这些 Schema 通常都不能修改&#xff0c;用户只有在创建时才能定义 Schema。Schema 的作用是告诉数据库使用者所希望的表结构&#xff0c;确保…...

Web网页开发-总结笔记2

28.为什么会出现浮动&#xff1f;浮动会带来哪些问题&#xff1f; 1)为什么会出现浮动&#xff1a; 为了页面排版时块元素同行显示 2)浮动带来的问题&#xff1a; 父元素高度崩塌29.清除浮动的方法 (额外标签法、父级overflow、after伪元素、双伪元素&#xff09; &#xff08…...

C#的StringBuilder方法

一、StringBuilder方法 StringBuilder方法Append()向此实例追加指定对象的字符串表示形式。AppendFormat()向此实例追加通过处理复合格式字符串&#xff08;包含零个或更多格式项&#xff09;而返回的字符串。 每个格式项都由相应的对象自变量的字符串表示形式替换。AppendJoi…...

美格智能5G RedCap模组SRM813Q通过广东联通5G创新实验室测试认证

近日&#xff0c;美格智能5G RedCap轻量化模组SRM813Q正式通过广东联通5G创新实验室端到端的测试验收&#xff0c;获颁测评证书。美格智能已连续通过业内两家权威实验室的测试认证&#xff0c;充分验证SRM813Q系列模组已经具备了成熟的商用能力&#xff0c;将为智慧工业、安防监…...

MVCC 并发控制原理-源码解析(非常详细)

基础概念 并发事务带来的问题 1&#xff09;脏读&#xff1a;一个事务读取到另一个事务更新但还未提交的数据&#xff0c;如果另一个事务出现回滚或者进一步更新&#xff0c;则会出现问题。 2&#xff09;不可重复读&#xff1a;在一个事务中两次次读取同一个数据时&#xff0c…...

通过国家网络风险管理方法提供安全的网络环境

印度尼西亚通过讨论网络安全法草案启动了其战略举措。不过&#xff0c;政府和议会尚未就该法案的多项内容达成一致。另一方面&#xff0c;制定战略性、全面的网络安全方法的紧迫性从未像今天这样重要。 其政府官方网站遭受了多起网络攻击&#xff0c;引发了人们对国家网络安全…...

input中typedate的属性都有那些

自我扩展‘ type 中date属性 自我 控制编辑区域的 ::-webkit-datetime-edit { padding: 1px; background: url(…/selection.gif); }控制年月日这个区域的 ::-webkit-datetime-edit-fields-wrapper { background-color: #eee; }这是控制年月日之间的斜线或短横线的 ::-webki…...

将PPT4页并排成1页

将PPT4页并排成1页打印 解决方法: 方法一 在打印时选择&#xff1a; 打开 PPT&#xff0c;点击文件选项点击打印点击整页幻灯片点击4张水平放置的幻灯平页面就会显示4张PPT显示在一张纸上 方法二 另存为PDF&#xff1a; 打开电脑上的目标PPT文件&#xff0c;点击文件点击…...

iPhone 恢复出厂设置后如何恢复数据

如果您在 iPhone 上执行了恢复出厂设置&#xff0c;您会发现所有旧数据都被清除了。这对于清理混乱和提高设备性能非常有用&#xff0c;但如果您忘记保存重要文件&#xff0c;那就是坏消息了。 恢复出厂设置后可以恢复数据吗&#xff1f;是的&#xff01;幸运的是&#xff0c;…...

欧洲最好的AI大模型:Mistral 7B!(开源、全面超越Llama 2)

你可能已经听说过Meta&#xff08;原Facebook&#xff09;的Llama 2&#xff0c;这是一款拥有13亿参数的语言模型&#xff0c;能够生成文本、代码、图像等多种内容。 但是你知道吗&#xff0c;有一家法国的创业公司Mistral AI&#xff0c;推出了一款只有7.3亿参数的语言模型&am…...

dede网站地图位置/关键词排名代发

2019独角兽企业重金招聘Python工程师标准>>> 其实微博是个好东西&#xff0c;关注一些技术博主之后&#xff0c;你不用再逛好多论坛了&#xff0c;因为一些很好的文章微博会告诉你&#xff0c;最近看到酷勤网推荐的一篇文章《30个提 高Web程序执行效率的好经验》&am…...

网站开发类投标文件/美国站外推广网站

下面就来看看什么是 JAR 文件包吧: 1. JAR 文件包 JAR 文件就是 Java Archi...

企业网站模板文件管理/小程序怎么引流推广

发现把含main()函数放到gdu_xmp下就编译正常&#xff0c;放到工程根目录下编译报错&#xff0c;好像找不到含main()的.c文件...

wordpress使用百度云存储/网站关键词

我想要做的是通过ajax和php调用一些数据库数据.但是ajax调用不起作用,我无法在网上找到解决方案.所以这是我的代码&#xff1a;test.php的include_once db_class.php;$cat $_GET[cat];$dbconn new dbconn(localhost, root, somepsw, blog);$dbconn->set_query("selec…...

网站gif图标/网络营销策划目的

基础规范【建议】使用InnoDB存储引擎【强制】无特殊要求必须使用UTF8字符集【强制】数据表、数据字段必须加入中文注释【强制】禁止使用存储过程、视图、触发器、Event。特殊情况申请评审【强制】不在数据库做运算&#xff0c;cpu计算务必移至业务层命名规范【建议】 命名使用具…...

陕西交通建设集团网站/谷歌搜索引擎大全

将HTML加载到webview后,我会在布局的右侧和底部出现白色条纹.对于正确的我使用以下方法解决它&#xff1a;setScrollBarStyle(WebView.SCROLLBARS_INSIDE_OVERLAY);但是,我尝试了很多选项来删除底部的一个没有成功.即使在我阅读了所有相关帖子之后.如果需要更多代码,请告诉我.a…...