当前位置: 首页 > 编程日记 > 正文

【C++】多线程与异步编程【四】

文章目录

  • 【C++】多线程与异步编程【四】
      • 0.三问
      • 1.什么是异步编程?
      • 1.1同步与异步
      • 1.2 **阻塞与非阻塞**
      • 2、如何使用异步编程
      • 2.1 使用全局变量与条件变量传递结果
        • 实例1:
      • 2.2 使用promise与future传递结果
        • 实例2
        • 实例3
      • 2.3使用packaged_task与future传递结果
        • 实例4
      • 2.4 使用async传递结果
        • 实例5
      • 3.小结

【C++】多线程与异步编程【四】

0.三问

同步,异步,多线程之间是什么关系?异步比同步高效在哪?多线程比单线程高效在哪? 捋一下, 想一下怎么回答。

1.什么是异步编程?

1.1同步与异步

前面谈到并发,互斥锁与条件变量, 前面提到的线程同步主要是为了解决对共享数据的竞争访问问题,所以线程同步主要是对共享数据的访问同步化(按照既定的先后次序,一个访问需要阻塞等待前一个访问完成后才能开始)。这篇文章谈到的异步编程主要是针对任务或线程的执行顺序,也即一个任务不需要阻塞等待上一个任务执行完成后再开始执行,程序的执行顺序与任务的排列顺序是不一致的。下面从任务执行顺序的角度解释下同步与异步的区别:

  • 同步:就是在发出一个调用时,在没有得到结果之前,该调用就不返回。但是一旦调用返回,就得到返回值了。换句话说,就是由调用者主动等待这个调用的结果。
  • 异步:调用在发出之后,这个调用就直接返回了,所以没有返回结果。换句话说,当一个异步过程调用发出后,调用者不会立刻得到结果。 而是在调用发出之后,被调用者通过“状态”、“通知”、“回调”三种途径通知调用者。

可以使用哪一种途径依赖于被带调用者的实现,除非被调用者提供多种选择,否则不受调用者控制。如果被调用者用状态来通知,那么调用者就需要每隔一定时间检查一次,效率就很低。如果使用通知和回调的方式,效率则很高。因为被调用者几乎不需要做额外的操作。

在这里插入图片描述

举个栗子:
你打电话问书店老板有没有《程序员的自我修养》这本书,如果是同步通信机制,书店老板会说,你稍等,”我查一下",然后开始查啊查,等查好了(可能是5秒,也可能是一天)告诉你结果(返回结果)。而异步通信机制,书店老板直接告诉你我查一下啊,查好了打电话给你,然后直接挂电话了(不返回结果)。然后查好了,他会主动打电话给你。在这里老板通过“回电”这种方式来回调。

1.2 阻塞与非阻塞

阻塞和非阻塞关注的是程序在等待调用结果(消息,返回值)时的状态。

阻塞调用是指调用结果返回之前,当前线程会被挂起。调用线程只有在得到结果之后才会返回。

非阻塞调用指在不能立刻得到结果之前,该调用不会阻塞当前线程。

举个栗子:

爱喝茶的老张,有两把水壶(普通水壶,简称水壶;会响的水壶,简称响水壶)。

  1. 老张把水壶放到火上,立等水开,然后泡茶。(同步阻塞)老张觉得自己有点傻。
  2. 老张把水壶放到火上,去客厅看电视,时不时去厨房看看水开没有,然后泡茶。(同步非阻塞)。
  3. 老张还是觉得自己有点傻,于是变高端了,买了把会响笛的那种水壶。水开后,能大声发出嘀~~~的噪音。
  4. 老张把响水壶放到火上,立等水开泡茶。(异步阻塞)。
  5. 老张觉得这样傻等意义不大,老张把响水壶放到火上,去客厅看电视,水壶响后泡茶。(异步非阻塞)。
  • 这里所谓同步异步,只是对于事件烧水和事件看电视。普通水壶,同步,指水壶烧水(线程)和来张(主线程)同时开始;响水壶,异步。虽然都能干活,但响水壶可以在自己完工之后,烧水事件,提示老张(主线程)水开了。这是普通水壶所不能及的。同步只能让调用者去轮询自己(情况2中),造成老张效率的低下。
  • 所谓阻塞非阻塞,仅仅对于老张的状态(主线程)而言。立等的老张,阻塞,主线程不能进行其他工作;看电视的老张,非阻塞,主线程可以进行其他工作。
    情况1和情况3中老张就是阻塞的,主线程(老张的状态)不能去做其他任何事情,媳妇喊他都不知道。虽然4中响水壶是异步的,可对于立等的老张没有太大的意义。所以一般异步是配合非阻塞使用的,这样才能发挥异步的效用。

这里针对多核的CPU在阻塞的情况下,计算量比较大的情况下,可以采用异步的方法,在将第i次的烧水和泡茶采用异步实现,使得喝茶这个目标的实现可以更加的高效。

2、如何使用异步编程

在线程库< thread >中并没有获得线程执行结果的方法,通常情况下,线程调用者需要获得线程的执行结果或执行状态,以便后续任务的执行。那么,通过什么方式获得被调用者的执行结果或状态呢?

2.1 使用全局变量与条件变量传递结果

前面谈到的条件变量具有“通知–唤醒”功能,可以把执行结果或执行状态放入一个全局变量中,当被调用者执行完任务后,通过条件变量通知调用者结果或状态已更新,可以使用了。

实例1:

//future1.cpp 使用全局变量传递被调用线程返回结果,使用条件变量通知调用线程已获得结果#include <vector>
#include <numeric>
#include <iostream>
#include <chrono>
#include <thread>
#include <mutex>
#include <condition_variable>int res = 0;						//保存结果的全局变量
std::mutex mu;						//互斥锁全局变量
std::condition_variable cond;       //全局条件变量void accumulate(std::vector<int>::iterator first,std::vector<int>::iterator last)
{int sum = std::accumulate(first, last, 0);      //标准库求和函数std::unique_lock<std::mutex> locker(mu);res = sum;locker.unlock();cond.notify_one();              // 向一个等待线程发出“条件已满足”的通知
}int main()
{std::vector<int> numbers = { 1, 2, 3, 4, 5, 6 };std::thread work_thread(accumulate, numbers.begin(), numbers.end());std::unique_lock<std::mutex> locker(mu);//如果条件变量被唤醒,检查结果是否被改变,为真则直接返回,为假则继续等待cond.wait(locker, [](){ return res;});   std::cout << "result=" << res << '\n';locker.unlock();work_thread.join();//阻塞等待线程执行完成getchar();return 0;
}
result=21

从上面的代码可以看出,虽然也实现了获取异步任务执行结果的功能,但需要的全局变量较多,多线程间的耦合度也较高,编写复杂程序时容易引入bug。有没有更好的方式实现异步编程呢?C++ 11新增了一个< future >库函数为异步编程提供了很大的便利。

2.2 使用promise与future传递结果

< future >头文件功能允许对特定提供者设置的值进行异步访问,可能在不同的线程中。
这些提供程序(要么是promise 对象,要么是packaged_task对象,或者是对异步的调用async)与future对象共享共享状态:提供者使共享状态就绪的点与future对象访问共享状态的点同步。< future >头文件的结构如下:
在这里插入图片描述

详细资料见此

在这里插入图片描述

实例2

std::promise< T >构造时,产生一个未就绪的共享状态(包含存储的T值和是否就绪的状态)。可设置T值,并让状态变为ready。也可以通过产生一个future对象获取到已就绪的共享状态中的T值。继续使用上面的程序示例,改为使用promise传递结果,修改后的代码如下:

实例3

//future2.cpp 使用promise传递被调用线程返回结果,通过共享状态变化通知调用线程已获得结果#include <vector>
#include <thread>
#include <future>
#include <numeric>
#include <iostream>
#include <chrono>void accumulate(std::vector<int>::iterator first,std::vector<int>::iterator last,std::promise<int> accumulate_promise)
{int sum = std::accumulate(first, last, 0);accumulate_promise.set_value(sum);  // 将结果存入,并让共享状态变为就绪以提醒future
}int main()
{// 演示用 promise<int> 在线程间传递结果。std::vector<int> numbers = { 1, 2, 3, 4, 5, 6 };std::promise<int> accumulate_promise;std::future<int> accumulate_future = accumulate_promise.get_future();std::thread work_thread(accumulate, numbers.begin(), numbers.end(),std::move(accumulate_promise));accumulate_future.wait();  //等待结果std::cout << "result=" << accumulate_future.get() << '\n';work_thread.join();  //阻塞等待线程执行完成getchar();return 0;
}
result=21

std::promise< T >对象的成员函数get_future()产生一个std::future< T >对象,代码示例中已经展示了future对象的两个方法:wait()与get(),下面给出更多操作函数供参考:

在这里插入图片描述

值得注意的是,std::future< T >在多个线程等待时,只有一个线程能获取等待结果。当需要多个线程等待相同的事件的结果(即多处访问同一个共享状态),需要用std::shared_future< T >来替代std::future < T >,std::future< T >也提供了一个将future转换为shared_future的方法f.share(),但转换后原future状态失效。这有点类似于智能指针std::unique_ptr< T >与std::shared_ptr< T >的关系,使用时需要留心。

2.3使用packaged_task与future传递结果

除了为一个任务或线程提供一个包含共享状态的变量,还可以直接把共享状态包装进一个任务或线程中。这就需要借助std::packaged_task< Func >来实现了,其具体用法如下:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-CpRCBIyA-1609675314863)(C:\Users\guoqi\AppData\Roaming\Typora\typora-user-images\1609669883139.png)]

std::packaged_task< Func >构造时绑定一个函数对象,也产生一个未就绪的共享状态。通过thread启动或者仿函数形式启动该函数对象。但是相比promise,没有提供set_value()公用接口,而是当执行完绑定的函数对象,其执行结果返回值或所抛异常被存储于能通过 std::future 对象访问的共享状态中。继续使用上面的程序示例,改为使用packaged_task传递结果,修改后的代码如下:

实例4

//future3.cpp 使用packaged_task传递被调用线程返回结果,通过共享状态变化通知调用线程已获得结果#include <vector>
#include <thread>
#include <future>
#include <numeric>
#include <iostream>
#include <chrono>int accumulate(std::vector<int>::iterator first,std::vector<int>::iterator last)
{int sum = std::accumulate(first, last, 0);return sum;
}int main()
{// 演示用 packaged_task 在线程间传递结果。std::vector<int> numbers = { 1, 2, 3, 4, 5, 6 };std::packaged_task<int(std::vector<int>::iterator,std::vector<int>::iterator)> accumulate_task(accumulate);std::future<int> accumulate_future = accumulate_task.get_future();std::thread work_thread(std::move(accumulate_task), numbers.begin(), numbers.end());accumulate_future.wait();  //等待结果std::cout << "result=" << accumulate_future.get() << '\n';work_thread.join();  //阻塞等待线程执行完成getchar();return 0;
}
result=21

一般不同函数间传递数据时,主要是借助全局变量、返回值、函数参数等来实现的。上面第一种方法使用全局变量传递数据,会使得不同函数间的耦合度较高,不利于模块化编程。后面两种方法分别通过函数参数与返回值来传递数据,可以降低函数间的耦合度,使编程和维护更简单快捷。

2.4 使用async传递结果

前面介绍的std::promise< T >与std::packaged_task< Func >已经提供了较丰富的异步编程工具,但在使用时既需要创建提供共享状态的对象(promise与packaged_task),又需要创建访问共享状态的对象(future与shared_future),还是觉得使用起来不够方便。有没有更简单的异步编程工具呢?future头文件也确实封装了更高级别的函数std::async,其具体用法如下:

  • std::future std::async(std::launch policy, Func, Args…)

std::async是一个函数而非类模板,其函数执行完后的返回值绑定给使用std::async的std::futrue对象(std::async其实是封装了thread,packged_task的功能,使异步执行一个任务更为方便)。Func是要调用的可调用对象(function, member function, function object, lambda),Args是传递给Func的参数,std::launch policy是启动策略,它控制std::async的异步行为,我们可以用三种不同的启动策略来创建std::async:

  • std::launch::async参数 保证异步行为,即传递函数将在单独的线程中执行;
  • std::launch::deferred参数 当其他线程调用get()/wait()来访问共享状态时,将调用非异步行为;
  • std::launch::async | std::launch::deferred参数 是默认行为(可省略)。有了这个启动策略,它可以异步运行或不运行,这取决于系统的负载。

继续使用上面的程序示例,改为使用std::async传递结果,修改后的代码如下:

实例5

//future4.cpp 使用async传递被调用线程返回结果#include <vector>
#include <thread>
#include <future>
#include <numeric>
#include <iostream>
#include <chrono>int accumulate(std::vector<int>::iterator first,std::vector<int>::iterator last)
{int sum = std::accumulate(first, last, 0);return sum;
}int main()
{// 演示用 async 在线程间传递结果。std::vector<int> numbers = { 1, 2, 3, 4, 5, 6 };auto accumulate_future = std::async(std::launch::async, accumulate, numbers.begin(), numbers.end());		//auto可以自动推断变量的类型std::cout << "result=" << accumulate_future.get() << '\n';getchar();return 0;
}
result=21

从上面的代码可以看出使用std::async能在很大程度上简少编程工作量,使我们不用关注线程创建内部细节,就能方便的获取异步执行状态和结果,还可以指定线程创建策略。所以,我们可以使用std::async替代线程的创建,让它成为我们做异步操作的首选。

此外,还有什么机制可以通过底层实现,提高性能,解决锁机制的问题,下面将会学习基于原子数据类型和对应的原子操作无锁编程的的思想。

3.小结

  1. 多线程比单线程高效的原因就是利用了CPU的多核计算把一个大的任务分而治之从而加速任务计算。

  2. 异步比同步高效的原因是前者释放了调用线程,让调用线程可以做更多的事情而不至于被windows强制休眠浪费线程资源。

  3. 就能方便的获取异步执行状态和结果,还可以指定线程创建策略。所以,我们可以使用std::async替代线程的创建,让它成为我们做异步操作的首选。

  4. 此外,还有什么机制可以通过底层实现,提高性能,解决锁机制的问题,下面将会学习基于原子数据类型和对应的原子操作无锁编程的的思想。

相关文章:

[LintCode] Maximum Subarray 最大子数组

Given an array of integers, find a contiguous subarray which has the largest sum. Notice The subarray should contain at least one number. Have you met this question in a real interview? YesExample Given the array [−2,2,−3,4,−1,2,1,−5,3], the contiguo…

图像补运算:反色处理

cv::Mat inverseColor1(cv::Mat srcImage) {cv::Mat tempImage srcImage.clone();int row tempImage.rows;int col tempImage.cols;// 对各个像素点遍历进行取反for (int i 0; i < row; i){for (int j 0; j < col; j){// 分别对各个通道进行反色处理tempImage.at<…

2018-2019-2 网络对抗技术 20165239Exp3 免杀原理与实践

2018-2019-2 网络对抗技术 20165239 Exp3 免杀原理与实践 win10 ip地址 192.168.18.1 fenix ip地址为 192.168.18.128 &#xff08;1&#xff09;杀软是如何检测出恶意代码的&#xff1f; •根据计算机病毒课程知道了每个病毒都有其对应的特征码&#xff0c;杀软是根据这些特征…

【C++】多线程与原子操作和无锁编程【五】

【C】多线程与原子操作和无锁编程【五】 1、何为原子操作 前面介绍了多线程间是通过互斥锁与条件变量来保证共享数据的同步的&#xff0c;互斥锁主要是针对过程加锁来实现对共享资源的排他性访问。很多时候&#xff0c;对共享资源的访问主要是对某一数据结构的读写操作&#…

jquery中ajax的dataType属性包括哪几项

参考ajax api文档&#xff1a;http://www.w3school.com.cn/jquery/ajax_ajax.asp dataType类型&#xff1a;String预期服务器返回的数据类型。如果不指定&#xff0c;jQuery 将自动根据 HTTP 包 MIME 信息来智能判断&#xff0c;比如 XML MIME 类型就被识别为 XML。在 1.4 中&a…

图像补运算:ptr反色处理

cv::Mat inverseColor3(cv::Mat srcImage) {cv::Mat tempImage srcImage.clone();int row tempImage.rows;// 将3通道转换为单通道int nStep tempImage.cols * tempImage.channels();for(int i 0; i < row; i) {// 取源图像的指针const uchar* pSrcData srcImage.ptr&l…

Android 在运行时请求权限

2019独角兽企业重金招聘Python工程师标准>>> 从 Android 6.0&#xff08;API 级别 23&#xff09;开始&#xff0c;用户开始在应用运行时向其授予权限&#xff0c;而不是在应用安装时授予。此方法可以简化应用安装过程&#xff0c;因为用户在安装或更新应用时不需要…

Markdown解决图片存储问题

文章目录Markdown1.前言2.图片引用方式方式1&#xff1a;可以任意比例放缩图片方式2&#xff1a;原比例引用图片3.推荐公式编辑器4.此外简单介绍下Markdown的一种轻量化工具Typora的使用方法。Markdown 1.前言 相信大家在使用Typora&#xff0c;经常会遇到图片编辑的问题&…

jenkins添加git源码目录时报Error performing command错误

简介 这是我在构建一个自动化部署项目中遇到的一个异常 解决步骤&#xff1a; 1、进入的jenkins的home目录&#xff0c;执行下面命令生成公钥和私钥 [rootjacky .jenkins]# ssh-keygen -t dsa 2、查看生成的公钥 [rootjacky .ssh]# cat /root/.ssh/id_dsa.pub ssh-dss AAAAB3Nz…

图像补运算:MatIterator_迭代器反色处理

#include <opencv2/opencv.hpp>#include <opencv2/video/background_segm.hpp>// 注意srcImage为3通道的彩色图片 cv::Mat inverseColor4(cv::Mat &srcImage) {cv::Mat tempImage srcImage.clone();// 初始化源图像迭代器 cv::MatConstIterator_<cv::Vec3…

浅谈同一家公司多个系统,共用登录用户名和密码

主要解决系统使用的加密方式不一致的问题&#xff0c; 比如几年前的系统A&#xff0c; 某某牵头无中生有的系统B 原先A用的php语言开发&#xff0c;比如叫做tap&#xff0c;是国外用来做项目管理的一款BS平台&#xff0c;&#xff08;和国内发禅道类似&#xff0c;省略***&…

Eigen/Matlab 使用小结

文章目录[Eigen Matlab使用小结](https://www.cnblogs.com/rainbow70626/p/8819119.html)Eigen初始化0.[官网资料](http://eigen.tuxfamily.org/index.php?titleMain_Page)1. Eigen Matlab矩阵定义2. Eigen Matlab基础使用3. Eigen Matlab特殊矩阵生成4. Eigen Matlab矩阵分块…

GitHUb 代码提交遇到的问题以及解决办法

git 添加代码出现以下错误&#xff1a; fatal: Unable to create F:/wamp/www/ThinkPhpStudy/.git/index.lock: File exists. If no other git process is currently running, this probably means a git process crashed in this repository earlier. Make sure no other git …

isContinuous 反色处理

cv::Mat inverseColor5(cv::Mat srcImage) {int row srcImage.rows;int col srcImage.cols;cv::Mat tempImage srcImage.clone();// 判断是否是连续图像&#xff0c;即是否有像素填充if( srcImage.isContinuous() && tempImage.isContinuous() ){row 1;// 按照行展…

阿里云智能对话分析服务

2019独角兽企业重金招聘Python工程师标准>>> 关于智能对话分析服务 智能对话分析服务 (Smart Conversation Analysis) 依托于阿里云语音识别和自然语言分析技术&#xff0c;为企业用户提供智能的对话分析服务&#xff0c;支持语音和文本数据的接入。可用于电话/在线…

【Smooth】非线性优化

文章目录非线性优化0 .case实战0.1求解思路0.2 g2o求解1. 状态估计问题1.1 最大后验与最大似然1.2 最小二乘的引出2. 非线性最小二乘2.1 一阶和二阶梯度法2.2 高斯牛顿法2.2 列文伯格-马夸尔特方法&#xff08;阻尼牛顿法)3 Ceres库的使用4 g2o库的使用非线性优化 0 .case实战…

.net 基于Jenkins的自动构建系统开发

先让我给描述一下怎么叫一个自动构建或者说是持续集成 &#xff1a; 就拿一个B/S系统的合作开发来说&#xff0c;在用SVN版本控制的情况下&#xff0c;每个人完成自己代码的编写&#xff0c;阶段性提交代码&#xff0c;然后测试-修改&#xff0c;最后到所有代码完工&#xff0c…

LUT 查表反色处理

cv::Mat inverseColor6(cv::Mat srcImage) {int row srcImage.rows;int col srcImage.cols;cv::Mat tempImage srcImage.clone();// 建立LUT 反色tableuchar LutTable[256];for (int i 0; i < 256; i)LutTable[i] 255 - i;cv::Mat lookUpTable(1, 256, CV_8U);uchar* p…

个人怎么发表期刊具体细节

目前在国内期刊发表&#xff0c;似乎已经成为非常普遍的一种现象&#xff0c;当然普通期刊发表的人数是比较多的&#xff0c;但是同样也有很多人选择核心期刊进行发表在众多期刊当中核心期刊&#xff0c;绝对是比较高级的刊物。很多人都想了解个人怎么发表期刊&#xff0c;那么…

【Math】P=NP问题

文章目录**P vs NP****0 PNP基本定义**0.1 Definition of NP-Completeness0.2 NP-Complete Problems0.3 NP-Hard Problems0.4 TSP is NP-Complete0.5 Proof**1 PNP问题****2 千禧年世纪难题****3 P类和NP类问题特征****4 多项式时间****5 现实中的NP类问题****6 大突破之NPC问题…

窥探react事件

写在前面 本文源于本人在学习react过程中遇到的一个问题&#xff1b;本文内容为本人的一些的理解&#xff0c;如有不对的地方&#xff0c;还请大家指出来。本文是讲react的事件&#xff0c;不是介绍其api&#xff0c;而是猜想一下react合成事件的实现方式 遇到的问题 class Eve…

Python内置方法

一、常用的内置方法 1、__new__ 和 __init__&#xff1a; __new__ 构造方法 、__init__初始化函数1、__new__方法是真正的类构造方法&#xff0c;用于产生实例化对象&#xff08;空属性&#xff09;。重写__new__方法可以控制对象的产 生过程。也就是说会通过继承object的new方…

【OpenCV 】Sobel 导数/Laplace 算子/Canny 边缘检测

canny边缘检测见OpenCV 【七】————边缘提取算子&#xff08;图像边缘提取&#xff09;——canny算法的原理及实现 1 Sobel 导数 1.1.1 原因 上面两节我们已经学习了卷积操作。一个最重要的卷积运算就是导数的计算(或者近似计算). 为什么对图像进行求导是重要的呢? 假设我…

RGB 转 HSV

#include <opencv2/core/core.hpp> #include <opencv2/highgui/highgui.hpp> #include <opencv2/imgproc/imgproc.hpp> #include <iostream> int main() {// 图像源读取及判断cv::Mat srcImage cv::imread("22.jpg");if (!srcImage.data) …

2017.1.9版给信息源新增:max_len、max_db字段

2017.1.8a版程序给信息源增加max_len、max_db字段&#xff0c;分别用于控制&#xff1a;获取条数、数据库保留条数。 max_len的说明见此图&#xff1a; max_db的说明见此图&#xff1a; 当max_len和max_db的设置不合理时&#xff08;比如max_len大于max_db&#xff0c;会导致反…

索引使用的几个原则

索引的使用尽量满足以下几个原则&#xff1a; 全值匹配最左前缀不在索引列上做任何操作(包括但不限于&#xff0c;计算&#xff0c;函数&#xff0c;类型转换)&#xff0c;会导致对应列索引失效。不适用索引中范围条件右边的列尽量使用覆盖索引使用不等于或者not in 的时候回变…

【OpenCV 】Remapping 重映射¶

目录 1.1目标 1.2 理论 1.3 代码 1.4 运行结果 1.1目标 展示如何使用OpenCV函数 remap 来实现简单重映射. 1.2 理论 把一个图像中一个位置的像素放置到另一个图片指定位置的过程. 为了完成映射过程, 有必要获得一些插值为非整数像素坐标,因为源图像与目标图像的像素坐标…

C# GUID的使用

GUID&#xff08;全局统一标识符&#xff09;是指在一台机器上生成的数字&#xff0c;它保证对在同一时空中的所有机器都是唯一的。通常平台会提供生成GUID的API。生成算法很有意思&#xff0c;用到了以太网卡地址、纳秒级时间、芯片ID码和许多可能的数字。GUID的唯一缺陷在于生…

文件名有规则情况读取

#include <iostream> #include <stdio.h> #include <stdlib.h> #include <opencv2/highgui/highgui.hpp> #include <opencv2/imgproc/imgproc.hpp> using namespace cv; using namespace std; int main() {// 定义相关参数const int num 4;char…

编写自己的SpringBoot-starter

2019独角兽企业重金招聘Python工程师标准>>> 前言 原理 首先说说原理&#xff0c;我们知道使用一个公用的starter的时候&#xff0c;只需要将相应的依赖添加的Maven的配置文件当中即可&#xff0c;免去了自己需要引用很多依赖类&#xff0c;并且SpringBoot会自动进行…