C++11异步编程
提示:文章写完后,目录可以自动生成,如何生成可参考右边的帮助文档
文章目录
- 前言
- 1、std::future和std::shared_future
- 1.1 std:future
- 1.2 std::shared_future
- 2、std::async
- 3、std::promise
- 4、std::packaged_task
前言
C++11提供了异步操作相关的类,主要有std:future、std:promise 和std:package task。std:future作为异步结果的传输通道,可以很方便地获取线程函数的返回值;。std:promise 用来包装一个值、将数据和future绑定起来,方便线程赋值; std:package_task 用来包装一个可调用对象,将函数和future绑定起来,以便异步调用。
1、std::future和std::shared_future
1.1 std:future
C++11中增加的线程,使得我们可以非常方便地创建和使用线程,但有时会有些不便,比如希望获取线程函数的返回结果,就不能直接通过thread.join()得到结果,这时就必须定义一个变量,在线程函数中去给这个变量赋值,然后执行join(),最后得到结果,这个过程是比较烦琐的。thread 库提供了future用来访问异步操作的结果,因为一个异步操作的结果不能马上获取,只能在未来某个时候从某个地方获取,这个异步操作的结果是一个未来的期待值,所以被称为future,future 提供了获取异步操作结果的通道。我们可以以同步等待的方式来获取结果,可以通过查询future 的状态( future_status) 来获取异步操作的结果。futrue_status有3种状态:
1、Deferred:异步操作还没开始
2、Ready:异步操作已经完成
3、Timeout:异步操作超时
future原型
template <class T> future;
template <class R&> future<R&>;
template <> future<void>
std::future是一个类模板
成员方法

通过future构造函数构造的future对象是无效的,此时调用future.get()可能会抛异常,需要我们捕获
int main()
{future<int> fu;try{cout << fu.get() << endl;}catch (const std::exception& e){cout << e.what() << endl;}return 0;
}

如果没有初始化future,则表明它是无效的,此时我们也可以使用valid判断future对象是否有效
int main()
{future<int> fu;if (fu.valid()){fu.get();}else{cout << "no state" << endl;}return 0;
}
std::asyanc是std::future的高级封装, 一般我们不会直接使用std::futrue,而是使用对std::future的高级封装std::async
mutex mtx;
string GetCurrentDateTime()
{SYSTEMTIME stime;GetLocalTime(&stime);stringstream ss;unique_lock<mutex> lock(mtx);ss << "[" << this_thread::get_id() << "]" << "current time: " << stime.wYear << "/" << stime.wMonth \<< "/" << stime.wDay << "/" << stime.wHour << "/" << stime.wMinute << "/" << stime.wSecond << endl;this_thread::sleep_for(chrono::seconds(2));return ss.str();
}int main()
{future<string> fu = async(GetCurrentDateTime);//执行一个异步任务来获取当前时间cout << this_thread::get_id() << " : main do something..." << endl;cout << fu.get() << endl;return 0;
}

上述代码中,main线程通过async创建一个新线程去执行异步任务(获取当前系统时间),在新线程执行异步任务的过程中,main线程可以不必阻塞,但在调用get方法时,如果新线程没有返回,就会被阻塞
我们可以通过wait_for进行超时等待返回结果,如果新线程还没返回,就做其他事情,再重复wait_for
mutex mtx;
string GetCurrentDateTime()
{SYSTEMTIME stime;GetLocalTime(&stime);stringstream ss;unique_lock<mutex> lock(mtx);ss << "[" << this_thread::get_id() << "]" << "current time: " << stime.wYear << "/" << stime.wMonth \<< "/" << stime.wDay << "/" << stime.wHour << "/" << stime.wMinute << "/" << stime.wSecond << endl;this_thread::sleep_for(chrono::seconds(3));//休眠3秒return ss.str();
}int main()
{std::future_status status;future<string> fu = async(GetCurrentDateTime);cout << this_thread::get_id() << " : main do something..." << endl;do{status = fu.wait_for(chrono::milliseconds(500));if (status == std::future_status::deferred){cout << "deferred:异步操作还没开始," << "main do something" << endl;}else if (status == std::future_status::timeout){cout << "timeout:在规定时间内异步操作还未完成," << "main do something" << endl;}else if (status == std::future_status::ready){cout << "ready:异步操作已经完成,当前时间为:" << fu.get();}} while (status != std::future_status::ready);return 0;
}

上述代码中,main线程调用了wait_for(chrono::milliseconds(500)),表示mian线程等待500ms,如果在500ms内,子线程完成异步任务,则立刻返回,如果超过500ms还没有完成异步任务,则不会继续等待,立刻返回
future不允许直接赋值或者拷贝,但允许移动赋值
void func(){}int main()
{future<void> fu1;future<void> fu2 = async(std::launch::async, func);if (fu2.valid()){cout << "fu2.valid()=true" << endl;cout << "move..." << endl;fu1 = std::move(fu2);cout << "fu2.valid()=" <<fu2.valid() << endl;cout << "fu1.valid()=" << fu1.valid() << endl;}else{cout << "fu2.valid()=false" << endl;}return 0;
}

get成功之后,future对象将不再有效
string func()
{return "fl";
}int main()
{future<string> fu = async(std::launch::async, func);if (fu.valid()){cout << "fu.get() begin fu.valid()=" << fu.valid() << endl;cout << "fu.get()=" << fu.get() << endl;cout << "fu.get() after fu.valid()=" << fu.valid() << endl;}return 0;
}

1.2 std::shared_future
shared_futrue原型
template <class T> shared_future;
template <class R&> shared_future<R&>;
template <> shared_future<void>;
C++11中的std: :shared_future是个模板类。与std::future类似,std::shared_future提供了一种访问异步操作结果的机制。不同于std::future,std::shared_futrue允许多个线程等待同一个共享状态。不同于std::future仅支持移动操作,std::shared_future既支持移动操作也支持拷贝操作,而多个std::shared_future对象可以引用相同的共享状态
前面谈到,对于std::future对象,如果就绪,使用get后,future对象就无效了,如果再使用get,则会抛异常。使用std::shared_future就很好的解决了这个问题
string func()
{return "fl";
}int main()
{shared_future<string> fu = async(std::launch::async, func);if (fu.valid()){cout << "fu.get()=" << fu.get() << endl;cout << "fu.get()=" << fu.get() << endl;cout << "fu.get()=" << fu.get() << endl;}else{cout << "fu.valid()=false" << endl;}return 0;
}

支持拷贝和赋值
string func()
{return "fl";
}int main()
{shared_future<string> fu = async(std::launch::async, func);shared_future<string> fu1 = fu;shared_future<string> fu2(fu1);if (fu.valid()){cout << "fu.get()=" << fu.get() << endl;cout << "fu1.get()=" << fu.get() << endl;cout << "fu2.get()=" << fu.get() << endl;}else{cout << "fu.valid()=false" << endl;}return 0;
}

2、std::async
std::async比std::future和std::promise、std::packaged和std::thread更高一层,它可以用来直接创建异步的task,异步任务返回的结果也保存在std::future中,需要获取异步任务的结果时,只需要调用future.get()方法即可,如果不关注异步任务的结果,只是简单地等待任务完成时的话,则调用future.wait()方法
async原型
template <class Fn, class... Args>future<typename result_of<Fn(Args...)>::type>async (Fn&& fn, Args&&... args);template <class Fn, class... Args>future<typename result_of<Fn(Args...)>::type>async (launch policy, Fn&& fn, Args&&... args);
后者比前者多了一个参数:launch policy,这个表示线程的创建策略,有两种策略,默认的策略是立即创建线程
- std::launch::async:在调用async时就开始创建线程
- std::launch::deferred:延迟加载方式创建线程。在调用async时不创建线程,知道调用了future的get或者wait时才创建线程
第二个参数:表示线程的入口函数,必须是一个可调用对象
第三个参数:表示线程函数的参数
std::async的操作,其实相当于封装了std::promise、std::packaged_task加上std::thread
使用std::launch::deferred作为参数
mutex mtx;
string GetCurrentDateTime()
{SYSTEMTIME stime;GetLocalTime(&stime);stringstream ss;unique_lock<mutex> lock(mtx);ss << "[" << this_thread::get_id() << "]" << "current time: " << stime.wYear << "/" << stime.wMonth \<< "/" << stime.wDay << "/" << stime.wHour << "/" << stime.wMinute << "/" << stime.wSecond << endl;cout << "in GetCurrentDateTime()" << endl;return ss.str();
}int main()
{future<string> fu = async(std::launch::deferred, GetCurrentDateTime);this_thread::sleep_for(chrono::milliseconds(500));cout << "main id is " << this_thread::get_id() << endl;cout << fu.get() << endl;return 0;
}

可以看到GetCurrentDateTime()也是有main线程执行的。在上述的执行顺序中,main线程先打印了自己的线程id,然后调用fu.get()时进入GetCurrentDateTime()内部,完成任务后才打印的时间。因此就能很好的证明deferred参数确实是在创建async时,不创建线程执行任务,而是在调用get方法时,由main线程去执行异步任务。
将参数改为async,执行结果为:

可以看到,在调用async时就开始创建线程去执行异步任务。
如果我们没有指定第一个参数,默认是async | deferred,具体采用哪个参数,取决于操作系统

3、std::promise
std::promise将数据和future绑定起来,为获取线程函数中的某个值提供便利,在线程函数中为外面传进来的promise赋值,在线程函数执行完成之后就可以通过promise的future获取该值了。值得注意的是:取值是间接地通过promise内部提供的future来获取的。
std::promise原型
template <class T> promise; //空模板
template <class R&> promise<R&>; //用于线程间交流对象
template <> promise<void>; //用于交流无状态事件
成员方法

举个例子,简单了解一下promise和future是如何关联的
void func1(std::promise<int>& promObj)
{cout << this_thread::get_id() << "设置promise的值" << endl;promObj.set_value(5);
}void func2(std::future<int>& fut)
{cout << this_thread::get_id() << "访问promise的值" << endl;cout << this_thread::get_id() << ":" << fut.get() << endl;
}int main()
{std::promise<int> prom;std::future<int> fut = prom.get_future();std::thread t1(func1, std::ref(prom));std::thread t2(func2, std::ref(fut));t1.join();t2.join();return 0;
}

在上述代码中,我们定义一个std::promise<int> prom,并让其与std::future<int> fut进行关联。让t1线程去调用fun11,t2线程去调用func2。如果t2线程在func2中调用fut.get()时,而fut并没有被设置,此时f2线程会被阻塞,直到t1线程在func1中调用promObj.set_value(5),例如:

std::promise的operator=没有拷贝语义,即std::promise的普通赋值函数是被delete掉了,只有move语义,因此std::promise对象是禁止拷贝的
int main()
{std::promise<int> prom;std::future<int> fut = prom.get_future();std::promise<int> prom1 = prom;return 0;
}

采用set_value_at_thread_exit演示一下死锁
mutex mtx;
void func1(std::promise<int>& promObj)
{this_thread::sleep_for(chrono::microseconds(500));promObj.set_value_at_thread_exit(1);unique_lock<mutex> lock(mtx);cout << this_thread::get_id() << "设置promise的值" << endl;
}void func2(std::future<int>& fut)
{unique_lock<mutex> lock(mtx);cout << this_thread::get_id() << "访问promise的值" << endl;cout << this_thread::get_id() << ":" << fut.get() << endl;
}int main()
{std::promise<int> prom;std::future<int> fut = prom.get_future();thread t1(func1, std::ref(prom));thread t2(func2, std::ref(fut));t1.join();t2.join();return 0;
}

为了让func2中的代码先执行,在func1的开始调用了sleep_for(chrono::microseconds(500)),休眠500微秒。首先f2线程拿到锁,执行到fut.get()会被阻塞,因为此时fut还未就绪。线程t1使用set_value_at_thread_exit(1),表明设置指定值为1,但是只有在t1线程结束时,才提醒。因此t1线程将被阻塞在unique_lock lock(mtx)上,而t2线程被阻塞在fut.get()上,所以产生了死锁。
使用set_value_at_thread_exit好处就是确保某个线程的退出,另一个线程才能被获取到某个值,例如:
void func1(std::promise<int>& promObj)
{promObj.set_value(1);cout << this_thread::get_id() << "设置promise的值" << endl;cout << "do something..." << endl;cout << "do something..." << endl;cout << "do something..." << endl;cout << "do something..." << endl;
}void func2(std::future<int>& fut)
{cout << this_thread::get_id() << "访问promise的值" << endl;cout << this_thread::get_id() << ":" << fut.get() << endl;
}int main()
{std::promise<int> prom;std::future<int> fut = prom.get_future();thread t1(func1, std::ref(prom));thread t2(func2, std::ref(fut));t1.detach();t2.join();return 0;
}

t1线程进行了分离,t2线程先结束,也就导致了main线程结束了,t1线程在后台执行,并没有将"do something…"打印到终端上。因此可以使用set_value_at_thread_exit使t2线程等待t1线程结束

一个线程将异常传递给另一个线程
void func1(std::promise<void>& p)
{try{cout << this_thread::get_id() << " throw error" << endl;throw exception("this is func1 throw error");}catch (const std::exception& e){p.set_exception_at_thread_exit(std::current_exception());}
}void func2(std::future<void>& fu)
{try{if (fu.valid())fu.get();}catch (const std::exception e){cout << this_thread::get_id() << " error is " << e.what() << endl;}
}int main()
{std::promise<void> p;std::future<void> fu = p.get_future();thread t1(func1, std::ref(p));thread t2(func2, std::ref(fu));t1.join();t2.join();return 0;
}

4、std::packaged_task
std:packaged _task 包装了一个可调用对象的包装类(如function、lambda expression、bind expression和another function object),将函数和future绑定起来,以便异步调用,它和std:promise在某种程度上有点像,promise保存了一个共享状态的值,而packaged task 保存的是一个函数。
std::packaged_task原型
template <class T> packaged_task; // undefined
template <class Ret, class... Args> class packaged_task<Ret(Args...)>;
成员函数

简单案例:
int f(int x, int y) { return std::pow(x, y); }void task_lambda()
{std::packaged_task<int(int, int)> task([](int a, int b) {return std::pow(a, b);});std::future<int> result = task.get_future();//auto result = task.get_future();task(2, 2);std::cout << "task_lambda:\t" << result.get() << '\n';
}void task_bind()
{std::packaged_task<int()> task(std::bind(f, 2, 3));std::future<int> result = task.get_future();//auto result = task.get_future();task();std::cout << "task_bind:\t" << result.get() << '\n';
}void task_thread()
{std::packaged_task<int(int, int)> task(f);std::future<int> result = task.get_future();std::thread task_td(std::move(task), 2, 4);task_td.join();std::cout << "task_thread:\t" << result.get() << '\n';
}int main()
{task_lambda();task_bind();task_thread();return 0;
}

相关文章:
C++11异步编程
提示:文章写完后,目录可以自动生成,如何生成可参考右边的帮助文档 文章目录前言1、std::future和std::shared_future1.1 std:future1.2 std::shared_future2、std::async3、std::promise4、std::packaged_task前言 C11提供了异步操作相关的类…...
20230310----重返学习-DOM元素的操作-时间对象-定时器
day-024-twenty-four-20230310-DOM元素的操作-时间对象-定时器 复习 获取元素 id document.getElementById() 类名 document.getElementsByClassName() 标签名 document.getElementsByTagName() name属性 document.getElementsByName() 选择器 document.querySelector()docum…...
江苏专转本转本人后悔排行榜
江苏专转本转本人后悔排行榜 一、复习的太迟: 后悔指数:五颗星。 复习越到最后,时间一天天变少,要复习的内容还有很多,很多人都后悔没有早早开始,总想着多给我两月一定会考上的。 担心时间不够用,那就努力利…...
【算法时间复杂度】学习记录
最近开算法课,开几篇文章记录一下算法的学习过程。 关于算法的重要性 学习计算机当程序员的话,在编程过程中是绕不开算法这个大矿山的,需要我们慢慢挖掘宝藏。 算法(Algorithm)是指用来操作数据、解决程序问题的一组…...
汽车车机芯片Linux系统内核编译问题总结
谈到车机,很多人会想到华为问界上装的大屏车机,号称车机的天花板,基于鸿蒙OS的,而今天谈到的车机芯片用的是linux内核Kernel,对于它的编译,很多人一时会觉得头大,的确如果工具不是很齐全,就会遇到这样那样的问题,但是过程都会有错误提示,按照错误提示基本可以解决,而…...
Android13 音量曲线调整
Android13 音量曲线调整 Android13 上配置文件的路径: /vendor/sprd/modules/audio/engineconfigurable_apm/工程目录/system/etc/audio_engine_config/audio_policy_engine_stream_volumes.xml /vendor/sprd/modules/audio/engineconfigurable_apm/工程目录/sys…...
OpenHarmony通过MQTT连接 “改版后的华为IoT平台”
一、前言 本篇文章我们使用的是BearPi-HM_Nano开发板:小熊派的主板+E53_IA1扩展板 源码用的是D6_iot_cloud_oc,点击下载BearPi-HM_Nano全量源码 那么为什么要写这篇呢? 前段时间看到OpenHarmony群里,经常有小伙伴问接入华为IoT平台的问题,他们无法正常连接到华为IoT平台等…...
SQS (Simple Queue Service)简介
mazon Simple Queue Service (SQS)是一种完全托管的消息队列服务,可以让你分离和扩展微服务、分布式系统和无服务应用程序。 在讲解SQS之前,首先让我们了解一下什么是消息队列。 消息队列 还是举一个电商的例子,一个用户在电商网站下单后付…...
高速PCB设计指南系列(三)
第一篇 高密度(HD)电路的设计 本文介绍,许多人把芯片规模的BGA封装看作是由便携式电子产品所需的空间限制的一个可行的解决方案,它同时满足这些产品更高功能与性能的要求。为便携式产品的高密度电路设计应该为装配工艺…...
【C++】C++11——左右值|右值引用|移动语义|完美转发
文章目录一、左值与右值1.概念2.引用3.注意二、右值引用的意义1.左值引用意义2.右值引用和移动语义3.容器新增三、万能引用四、完美转发一、左值与右值 1.概念 左值是什么?右值是什么? 左值是一个表示数据的表达式(如变量名或解引用的指针&…...
[ROC-RK3399-PC Pro] 手把手教你移植主线Buildroot(基于2023.02-rc3版本)
🍇 博主主页:Systemcall小酒屋🍇 博主简介:Neutionwei,C站嵌入式领域新星创作者之一,一枚热爱开源技术、喜欢分享技术心得的极客,注重简约风格,热衷于用简单的案例讲述复杂的技术&am…...
重温线性代数
前言 对于普通的数学工作者而言,掌握矩阵、线性空间的基本性质和用法比领会抽象的概念更实用。数学专业的同学需要全面深入学习近世代数的理论和演绎法则,例如模的概念和运算。 总之,我个人认为,不论是微积分、还是线性代数&…...
2023河北沃克HEGERLS甘肃金昌重型仓储项目案例|托盘式四向穿梭车智能密集存储系统在工业行业的创新应用
项目名称:自动化仓储托盘式四向穿梭车智能密集立体库项目 项目合作客户:甘肃省金昌市某集团企业 项目施工地域:甘肃省金昌市 设计与承建单位:河北沃克金属制品有限公司(自主品牌:海格里斯HEGERLS&#x…...
软件测试的案例分析 - 闰年5
文章目的 显示不同的博客能获得多少博客质量分 (这是关于博客质量分的测试 https://www.csdn.net/qc) 这个博客得了 83 分。怎么才能得到更多分数? 正文 我们谈了不少测试的名词, 软件是人写的, 测试计划和测试用例也是人写的, 人总会犯错误。错误发生…...
Linux文件基础I/O
文件IO文件的常识基础IO为什么要学习操作系统的文件操作C语言对于函数接口的使用接口函数介绍如何理解文件文件描述符重定向更新给模拟实现的shell增加重定向功能为什么linux下一切皆文件?文件的常识 1.空文件也要在磁盘占据空间 2.文件 内容 属性 3.文件操作 对…...
HTML看这一篇就够啦,HTML基础大全,可用于快速回顾知识,面试首选
HTML 1 基础 1.1 DOCTYPE <!DOCTYPE> 文档类型声明,作用就是告诉浏览器使用哪种HTML版本来显示网页。 <!DOCTYPE html> 这句代码的意思是: 当前页面采取的是 HTML5 版本来显示网页. 注意: 声明位于文档中的最前面的位置,处于 标签之前。 …...
Altium Designer(AD)软件使用记录05-PCB叠层设计
目录Altium Designer(AD)软件使用记录05-PCB叠层设计一、正片层和负片层的介绍1、正片层(Signal)2、负片层(Plane)3、内电层的分割实现二、正片层和负片层的内缩设计1、负片设置内缩20H原则2、正片铺铜设置内缩1、设置规则2、重新铺铜三、AD的层叠设计四、叠层设计需要注意的问…...
ArcGIS动态表格批量出图
一.产品介绍:ArcGIS动态表格扩展模块Mapping and Charting Solutions,可用于插入动态表格,与数据驱动结合,出图效率无敌。注:优先选择arcgis10.2.2。 二、下载连接: https://www.xsoftnet.com/share/a001CX…...
ChatGPT真神奇,但是也真焦虑
ChatGPT火爆ChatGPT的火爆程度不用说也知道。就目前来说,已经开始冲击各行业了,比如客服、智能助手、语言学习、自然语言处理等等等。。ChatGPT冲击冲击最高的可能就是中间这个段位的了。高段位无法取代,但是低段位,通过使用ChatG…...
mos管驱动与米勒平台介绍、消除
mos驱动设计 1.选择适当的驱动芯片 为了控制MOSFET,需要使用专门的驱动芯片。选择合适的芯片需要考虑MOSFET的电压和电流需求。常见的驱动芯片包括IR2110、IR2184、MIC4424等。 2.设计电路 在驱动电路中,需要加入一些电路元件来保证MOSFET的顺畅工作…...
工业安全零事故的智能守护者:一体化AI智能安防平台
前言: 通过AI视觉技术,为船厂提供全面的安全监控解决方案,涵盖交通违规检测、起重机轨道安全、非法入侵检测、盗窃防范、安全规范执行监控等多个方面,能够实现对应负责人反馈机制,并最终实现数据的统计报表。提升船厂…...
Python:操作 Excel 折叠
💖亲爱的技术爱好者们,热烈欢迎来到 Kant2048 的博客!我是 Thomas Kant,很开心能在CSDN上与你们相遇~💖 本博客的精华专栏: 【自动化测试】 【测试经验】 【人工智能】 【Python】 Python 操作 Excel 系列 读取单元格数据按行写入设置行高和列宽自动调整行高和列宽水平…...
QMC5883L的驱动
简介 本篇文章的代码已经上传到了github上面,开源代码 作为一个电子罗盘模块,我们可以通过I2C从中获取偏航角yaw,相对于六轴陀螺仪的yaw,qmc5883l几乎不会零飘并且成本较低。 参考资料 QMC5883L磁场传感器驱动 QMC5883L磁力计…...
macOS多出来了:Google云端硬盘、YouTube、表格、幻灯片、Gmail、Google文档等应用
文章目录 问题现象问题原因解决办法 问题现象 macOS启动台(Launchpad)多出来了:Google云端硬盘、YouTube、表格、幻灯片、Gmail、Google文档等应用。 问题原因 很明显,都是Google家的办公全家桶。这些应用并不是通过独立安装的…...
【项目实战】通过多模态+LangGraph实现PPT生成助手
PPT自动生成系统 基于LangGraph的PPT自动生成系统,可以将Markdown文档自动转换为PPT演示文稿。 功能特点 Markdown解析:自动解析Markdown文档结构PPT模板分析:分析PPT模板的布局和风格智能布局决策:匹配内容与合适的PPT布局自动…...
MODBUS TCP转CANopen 技术赋能高效协同作业
在现代工业自动化领域,MODBUS TCP和CANopen两种通讯协议因其稳定性和高效性被广泛应用于各种设备和系统中。而随着科技的不断进步,这两种通讯协议也正在被逐步融合,形成了一种新型的通讯方式——开疆智能MODBUS TCP转CANopen网关KJ-TCPC-CANP…...
Mac软件卸载指南,简单易懂!
刚和Adobe分手,它却总在Library里给你写"回忆录"?卸载的Final Cut Pro像电子幽灵般阴魂不散?总是会有残留文件,别慌!这份Mac软件卸载指南,将用最硬核的方式教你"数字分手术"࿰…...
基于Docker Compose部署Java微服务项目
一. 创建根项目 根项目(父项目)主要用于依赖管理 一些需要注意的点: 打包方式需要为 pom<modules>里需要注册子模块不要引入maven的打包插件,否则打包时会出问题 <?xml version"1.0" encoding"UTF-8…...
C++.OpenGL (10/64)基础光照(Basic Lighting)
基础光照(Basic Lighting) 冯氏光照模型(Phong Lighting Model) #mermaid-svg-GLdskXwWINxNGHso {font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}#mermaid-svg-GLdskXwWINxNGHso .error-icon{fill:#552222;}#mermaid-svg-GLd…...
AI编程--插件对比分析:CodeRider、GitHub Copilot及其他
AI编程插件对比分析:CodeRider、GitHub Copilot及其他 随着人工智能技术的快速发展,AI编程插件已成为提升开发者生产力的重要工具。CodeRider和GitHub Copilot作为市场上的领先者,分别以其独特的特性和生态系统吸引了大量开发者。本文将从功…...
