C++多线程入门及基础知识

news2025/1/18 7:38:02

什么是C++多线程

线程即操作系统进行CPU任务调度的最小单位。C++的多线程并发,简单理解的话就是,将任务的不同功能交由多个函数实现,创建多个线程,每个线程执行一个函数,一个任务就同时由不同线程执行。
在这里插入图片描述

什么时候使用多线程

程序使用并发的原因有两种:关注点分离或提高性能。当为了分离关注点而使用多线程时,设计线程的数量的依据,不再是依赖于CPU中的可用内核的数量,而是依据概念上的设计(依据功能的划分)。

不使用并发的唯一原因就是收益(性能的增幅)比不上成本(代码开发的脑力成本、时间成本,代码维护相关的额外成本)。运行越多的线程,操作系统需要为每个线程分配独立的栈空间,需要越多的上下文切换,这会消耗很多操作系统资源,如果在线程上的任务完成得很快,那么实际执行任务的时间要比启动线程的时间小很多,所以在某些时候,增加一个额外的线程实际上会降低,而非提高应用程序的整体性能,此时收益就比不上成本。

多线程基础知识

创建线程

首先引入头文件#include<thread>,之前一些编译器使用 C++ 11 的编译参数是 -std=c++11。
g++ -std=c++11 test.cpp
std::thread 默认构造函数,创建一个空的 std::thread 执行对象。
std::thread thread_object(callable)
一个可调用对象可以是以下三个中的任何一个:

  • 函数指针
  • 函数对象
  • lambda 表达式

定义 callable 后,将其传递给 std::thread 构造函数 thread_object。

只要创建了线程对象(前提是,实例化std::thread对象时传递了“函数名/可调用对象”作为参数),线程就开始执行。所以不应该在创建了线程后马上join, 这样会马上阻塞主线程,创建了线程和没有创建一样,应该在晚一点的位置调用join。

当线程启动后,一定要在和线程相关联的std::thread对象销毁前,对线程运用join()或者detach()方法。join()与detach()都是std::thread类的成员函数,是两种线程阻塞方法,两者的区别是是否等待子线程执行结束。新手先理解join就行,join()会阻塞主线程,等到该线程结束后主线程会继续运行。

创建线程举例

传参问题分析
总体来说,std::thread的构造函数会拷贝传入的参数:

  1. 当传入参数为基本数据类型(int,char,string等)时,会拷贝一份给创建的线程;

  2. 当传入参数为指针时,会浅拷贝一份给创建的线程,也就是说,只会拷贝对象的指针,不会拷贝指针指向的对象本身。

  3. 当传入的参数为引用时,实参必须用ref()函数处理后传递给形参,否则编译不通过,此时不存在“拷贝”行为。引用只是变量的别名,在线程中传递对象的引用,那么该对象始终只有一份,只是存在多个别名罢了(注意把引用与指针区别开:指针是一块内存指向另一块内存,指针侧重“指向”二字;引用是只有一块内存,存在多个别名。理解引用时不要想着别名“指向”内存,这是错误的理解,这样的理解会导致分不清指针和引用,别名与其本体侧重于“一体”二字,引用就是本体,本体就是引用,根本没有“指向”关系。);
    这是怎么回事呢?原来thread在传递参数时,是以右值传递的:template <class Fn, class… Args>
    explicit thread(Fn&& fn, Args&&… args)。
    划重点:Args&&… args
    很明显的右值引用,那么我们该如何传递一个左值呢?std::ref和std::cref很好地解决了这个问题。
    std::ref 可以包装按引用传递的值。std::cref 可以包装按const引用传递的值。

  4. 当传入的参数为类对象时,会拷贝一份给创建的线程。此时会调用类对象的拷贝构造函数。
    一些例子
    参数为引用

#include<thread>
#include<iostream>
using namespace std;
void proc(int& x)
{
	cout << x <<","<<&x<<endl;
}

int main()
{
	int a=10;
	cout<< a <<",,"<<&a<<endl;
	thread t1(proc,ref(a));
	t1.join();
	return 0;
} 

互斥量(锁)使用

互斥量是为了解决数据共享过程中可能存在的访问冲突的问题。因此可以通过加锁解锁来保证同一时刻只有一个线程能访问共享资源;使用锁的时候要注意,不能出现死锁的状况;

死锁就是多个线程争夺共享资源导致每个线程都不能取得自己所需的全部资源,从而程序无法向下执行。产生死锁的四个条件:

  • 互斥(资源同一时刻只能被一个进程使用)
  • 请求并保持(进程在请资源时,不释放自己已经占有的资源)
  • 不剥夺(进程已经获得的资源,在进程使用完前,不能强制剥夺)
  • 循环等待(进程间形成环状的资源循环等待关系)

临界区、信号量、互斥量(锁)的区别与联系:
三者都可以用来进行进程的同步和互斥。

临界区速度最快,但只能用于同一进程下不同线程,不能用于u不同进程;临界区可以确保某一代码段同一时刻只被一个线程执行:

EnterCriticalSection() 进入临界区

LeaveCriticalSection() 离开临界区

信号量多个线程同一时刻访问共享资源,进行线程的计数,确保同时访问资源的线程数目不超过上限,当访问数超过上限后,不发出信号量;

P操作 申请资源

V操作 释放资源

互斥量(锁)比临界区满,但支持不同进程间的同步与互斥;

互斥锁和读写锁的区别
互斥量mutex就是互斥锁,加锁的资源支持互斥访问;
shared_mutex读写锁把对共享资源的访问者划分成读者和写者,多个读线程能同时读取共享资源,但只有一个写线程能同时读取共享资源;shared_mutex通过lock_shared,unlock_shared进行读者的锁定与解锁;通过lock,unlock进行写者的锁定与解锁。

shared_mutex s_m;

std::string book;

void read()
{
	s_m.lock_shared();
	cout << book;
	s_m.unlock_shared();
}

void write()
{
	s_m.lock();
	book = "new context";
	s_m.unlock();
}

互斥锁举例

首先需要#include;(std::mutex和std::lock_guard都在头文件中声明。)

然后需要实例化std::mutex对象;

需要在进入临界区之前对互斥量加锁,退出临界区时对互斥量解锁;

#include<iostream>
#include<thread>
#include<mutex>
using namespace std;
mutex m;//实例化m对象,不要理解为定义变量
void proc1(int a)
{
    m.lock();
    cout << "proc1函数正在改写a" << endl;
    cout << "原始a为" << a << endl;
    cout << "现在a为" << a + 2 << endl;
    m.unlock();
}

void proc2(int a)
{
    m.lock();
    cout << "proc2函数正在改写a" << endl;
    cout << "原始a为" << a << endl;
    cout << "现在a为" << a + 1 << endl;
    m.unlock();
}
int main()
{
    int a = 0;
    thread t1(proc1, a);
    thread t2(proc2, a);
    t1.join();
    t2.join();
    return 0;
}

程序实例化mutex对象m,本线程调用成员函数m.lock()会发生下面 2 种情况: (1)如果该互斥量当前未上锁,则本线程将该互斥量锁住,直到调用unlock()之前,本线程一直拥有该锁。 (2)如果该互斥量当前被其他线程锁住,则本线程被阻塞,直至该互斥量被其他线程解锁,此时本线程将该互斥量锁住,直到调用unlock()之前,本线程一直拥有该锁。

不推荐实直接去调用成员函数lock(),因为如果忘记unlock(),将导致锁无法释放,使用lock_guard或者unique_lock则能避免忘记解锁带来的问题。其原理是:声明一个局部的std::lock_guard对象,在其构造函数中进行加锁,在其析构函数中进行解锁。最终的结果就是:创建即加锁,作用域结束自动解锁。从而使用std::lock_guard()就可以替代lock()与unlock()。

通过设定作用域,使得std::lock_guard在合适的地方被析构(在互斥量锁定到互斥量解锁之间的代码叫做临界区(需要互斥访问共享资源的那段代码称为临界区),临界区范围应该尽可能的小,即lock互斥量后应该尽早unlock),通过使用{}来调整作用域范围,可使得互斥量m在合适的地方被解锁:

#include<iostream>
#include<thread>
#include<mutex>
using namespace std;
mutex m;//实例化m对象,不要理解为定义变量
void proc1(int a)
{
    lock_guard<mutex> g1(m);//用此语句替换了m.lock();lock_guard传入一个参数时,该参数为互斥量,此时调用了lock_guard的构造函数,申请锁定m
    cout << "proc1函数正在改写a" << endl;
    cout << "原始a为" << a << endl;
    cout << "现在a为" << a + 2 << endl;
}//此时不需要写m.unlock(),g1出了作用域被释放,自动调用析构函数,于是m被解锁

**加粗样式**void proc2(int a)
{
    {
        lock_guard<mutex> g2(m);
        cout << "proc2函数正在改写a" << endl;
        cout << "原始a为" << a << endl;
        cout << "现在a为" << a + 1 << endl;
    }//通过使用{}来调整作用域范围,可使得m在合适的地方被解锁
    cout << "作用域外的内容3" << endl;
    cout << "作用域外的内容4" << endl;
    cout << "作用域外的内容5" << endl;
}
int main()
{
    int a = 0;
    thread t1(proc1, a);
    thread t2(proc2, a);
    t1.join();
    t2.join();
    return 0;
}

std::lock_gurad也可以传入两个参数,第一个参数为adopt_lock标识时,表示构造函数中不再进行互斥量锁定,因此此时需要提前手动锁定

#include<iostream>
#include<thread>
#include<mutex>
using namespace std;
mutex m;//实例化m对象,不要理解为定义变量
void proc1(int a)
{
    m.lock();//手动锁定
    lock_guard<mutex> g1(m,adopt_lock);
    cout << "proc1函数正在改写a" << endl;
    cout << "原始a为" << a << endl;
    cout << "现在a为" << a + 2 << endl;
}//自动解锁

void proc2(int a)
{
    lock_guard<mutex> g2(m);//自动锁定
    cout << "proc2函数正在改写a" << endl;
    cout << "原始a为" << a << endl;
    cout << "现在a为" << a + 1 << endl;
}//自动解锁
int main()
{
    int a = 0;
    thread t1(proc1, a);
    thread t2(proc2, a);
    t1.join();
    t2.join();
    return 0;
}

unique_lock
std::unique_lock类似于lock_guard,只是std::unique_lock用法更加丰富,同时支持std::lock_guard()的原有功能。使用std::unique_lock后可以手动lock()与手动unlock(); std::unique_lock的第二个参数,除了可以是adopt_lock,还可以是try_to_lock与defer_lock:

  • defer_lock: 始化了一个没有加锁的mutex
  • try_to_lock: 尝试去锁定,得保证锁处于unlock的状态,然后尝试现在能不能获得锁;尝试用mutx的lock()去锁定这个mutex,但如果没有锁定成功,会立即返回,不会阻塞在那里,并继续往下执行;
#include<iostream>
#include<thread>
#include<mutex>
using namespace std;
mutex m;
void proc1(int a)
{
	unique_lock<mutex> g1(m, defer_lock);//始化了一个没有加锁的mutex
	cout << "xxxxxxxx" << endl;
	g1.lock();//手动加锁,注意,不是m.lock();注意,不是m.lock(),m已经被g1接管了;
	cout << "proc1函数正在改写a" << endl;
	cout << "原始a为" << a << endl;
	cout << "现在a为" << a + 2 << endl;
	g1.unlock();//临时解锁
	cout << "xxxxx" << endl;
	g1.lock();
	cout << "xxxxxx" << endl;
}//自动解锁

void proc2(int a)
{
	unique_lock<mutex> g2(m, try_to_lock);//尝试加锁一次,但如果没有锁定成功,会立即返回,不会阻塞在那里,且不会再次尝试锁操作。
	if (g2.owns_lock()) {//锁成功
		cout << "proc2函数正在改写a" << endl;
		cout << "原始a为" << a << endl;
		cout << "现在a为" << a + 1 << endl;
	}
	else {//锁失败则执行这段语句
		cout << "" << endl;
	}
}//自动解锁

int main()
{
	int a = 0;
	thread t1(proc1, a);
	t1.join();
	//thread t2(proc2, a);
	//t2.join();
	return 0;
}

使用try_to_lock要小心,因为try_to_lock尝试锁失败后不会阻塞线程,而是继续往下执行程序,因此,需要使用if-else语句来判断是否锁成功,只有锁成功后才能去执行互斥代码段。而且需要注意的是,因为try_to_lock尝试锁失败后代码继续往下执行了,因此该语句不会再次去尝试锁。

异步线程

thread可以快速、方便地创建线程,但在async面前,就是小巫见大巫了。
async可以根据情况选择同步执行或创建新线程来异步执行,当然也可以手动选择。对于async的返回值操作也比thread更加方便。

使用async需要引入#include<future>库。

std::async是一个函数模板,用来启动一个异步任务,它返回一个std::future类模板对象,future对象起到了占位的作用(记住这点就可以了),占位是什么意思?就是说该变量现在无值,但将来会有值(好比你挤公交瞧见空了个座位,刚准备坐下去就被旁边的小伙给拦住了:“这个座位有人了”,你反驳道:”这不是空着吗?“,小伙:”等会人就来了“),刚实例化的future是没有储存值的,但在调用std::future对象的get()成员函数时,主线程会被阻塞直到异步线程执行结束,并把返回结果传递给std::future,即通过FutureObject.get()获取函数返回值。

相当于你去办政府办业务(主线程),把资料交给了前台,前台安排了人员去给你办理(std::async创建子线程),前台给了你一个单据(std::future对象),说你的业务正在给你办(子线程正在运行),等段时间你再过来凭这个单据取结果。过了段时间,你去前台取结果(调用get()),但是结果还没出来(子线程还没return),你就在前台等着(阻塞),直到你拿到结果(子线程return),你才离开(不再阻塞)。

#include <iostream>
#include <thread>
#include <mutex>
#include<future>
#include<Windows.h>
using namespace std;
double t1(const double a, const double b)
{
 double c = a + b;
 Sleep(3000);//假设t1函数是个复杂的计算过程,需要消耗3秒
 return c;
}

int main() 
{
 double a = 2.3;
 double b = 6.7;
 future<double> fu = async(t1, a, b);//创建异步线程线程,并将线程的执行结果用fu占位;
 cout << "正在进行计算" << endl;
 cout << "计算结果马上就准备好,请您耐心等待" << endl;
 cout << "计算结果:" << fu.get() << endl;//阻塞主线程,直至异步线程return
        //cout << "计算结果:" << fu.get() << endl;//取消该语句注释后运行会报错,因为future对象的get()方法只能调用一次。
 return 0;
}

shared_future
std::future与std::shard_future的用途都是为了占位,但是两者有些许差别。std::future的get()成员函数是转移数据所有权;std::shared_future的get()成员函数是复制数据。 因此: future对象的get()只能调用一次;无法实现多个线程等待同一个异步线程,一旦其中一个线程获取了异步线程的返回值,其他线程就无法再次获取。 std::shared_future对象的get()可以调用多次;可以实现多个线程等待同一个异步线程,每个线程都可以获取异步线程的返回值。

原子类型atomic

原子操作指“不可分割的操作”,也就是说这种操作状态要么是完成的,要么是没完成的,不存在“操作完成了一半”这种状况。互斥量的加锁一般是针对一个代码段,而原子操作针对的一般都是一个变量(操作变量时加锁防止他人干扰)。

std::atomic<>是一个模板类,使用该模板类实例化的对象,提供了一些保证原子性的成员函数来实现共享数据的常用操作。

可以这样理解: 在以前,定义了一个共享的变量(int i=0),多个线程会用到这个变量,那么每次操作这个变量时,都需要lock加锁,操作完毕unlock解锁,以保证线程之间不会冲突;但是这样每次加锁解锁、加锁解锁就显得很麻烦,那怎么办呢? 现在,实例化了一个类对象(std::atomic I=0)来代替以前的那个变量(这里的对象I你就把它看作一个变量,看作对象反而难以理解了),每次操作这个对象时,就不用lock与unlock,这个对象自身就具有原子性(相当于加锁解锁操作不用你写代码实现,能自动加锁解锁了),以保证线程之间不会冲突。

提到std::atomic<>,你脑海里就想到一点就可以了:std::atomic<>用来定义一个自动加锁解锁的共享变量(“定义”“变量”用词在这里是不准确的,但是更加贴切它的实际功能),供多个线程访问而不发生冲突。

//原子类型的简单使用
std::atomic<bool> b(true);
b=false;

线程池

不采用线程池时:

创建线程 -> 由该线程执行任务 -> 任务执行完毕后销毁线程。即使需要使用到大量线程,每个线程都要按照这个流程来创建、执行与销毁。

虽然创建与销毁线程消耗的时间 远小于 线程执行的时间,但是对于需要频繁创建大量线程的任务,创建与销毁线程 所占用的时间与CPU资源也会有很大占比。

为了减少创建与销毁线程所带来的时间消耗与资源消耗,因此采用线程池的策略:

程序启动后,预先创建一定数量的线程放入空闲队列中,这些线程都是处于阻塞状态,基本不消耗CPU,只占用较小的内存空间。

接收到任务后,任务被挂在任务队列,线程池选择一个空闲线程来执行此任务。

任务执行完毕后,不销毁线程,线程继续保持在池中等待下一次的任务。

参考资料

书籍:<C++ Concurrency in Action> 中文译本<C++并发编程实战>
某乎链接
C站参考

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/161876.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

微信小程序:骨架屏的实现方法

骨架屏是为了展示一个页面骨架而不含有实际的页面内容&#xff0c;从渲染效率上来讲&#xff0c;骨架屏它并不能使首屏渲染加快。由于骨架屏的一些使用又向用户渲染了额外的一些内容&#xff0c;这些内容是额外添加的、本来是不需要渲染的&#xff0c;它反而从整体上加长了首屏…

Windows 虚拟磁盘驱动开发(采用原始办法实现类似Storport框架的相同功能)

其实以前讲述windows平台下的磁盘驱动的开发挺多&#xff0c;而且时间也是非常早。以下连接&#xff1a;https://blog.csdn.net/fanxiushu/article/details/9903123?spm1001.2014.3001.5501https://blog.csdn.net/fanxiushu/article/details/11713357?spm1001.2014.3001.5501…

游戏开发 状态同步

【状态同步】1、将所有的操作发送给Server&#xff08;T1&#xff09;&#xff0c;由Server计算&#xff08;T2&#xff09;&#xff0c;并返回结果&#xff08;T3&#xff09;。权威服务器架构能够防止很多的作弊&#xff0c;但是直接用这种方法会让游戏的响应变得迟缓。如果 …

three games 之 桌球

接下来介绍一些 Vue4 中的一些进阶使用&#xff0c;希望对大家有所帮助&#xff0c;谢谢。 如果文中有不对、疑惑的地方&#xff0c;欢迎在评论区留言指正&#x1f33b; 一、项目结构 Vuex 并不限制你的代码结构。但是&#xff0c;它规定了一些需要遵守的规则&#xff1a; …

移动硬盘怎么分区?

硬盘分区指的是硬盘上被划分出来的区块&#xff0c;可用于分类存储各种数据。而我们日常购买的移动硬盘通常来说分为两种&#xff0c;一种是买回来已分好区的&#xff0c;还有一种是未经过分区的。如果移动硬盘没有经过分区&#xff0c;那么在将它连接到电脑的USB接口时&#x…

android四大组件之四-BroadCast实现原理分析

前言&#xff1a; 一开始的目标是解决各种各样的ANR问题的&#xff0c;但是我们知道&#xff0c;ANR总体上分有四种类型&#xff0c;这四种ANR类型有三种是和四大组件相对应的&#xff0c;所以&#xff0c;如果想了解ANR发生的根因&#xff0c;对安卓四大组件的实现原理必须要…

伙伴云与飞书、金山办公一同入选亿欧2022中国数字化企业服务商TOP50

近日&#xff0c;由中关村国家自主创新示范区展示中心、中关村会展与服务产业联盟与亿欧联合举办的SHOWTECH2022-WIM 创新者年会”在京顺利召开&#xff0c;会上&#xff0c;亿欧网重磅发布《2022世界创新奖榜单》。伙伴云凭借10年来为企业数字化转型赋能的成功经验和卓越贡献&…

数据结构学习-队列

坚持看完&#xff0c;结尾有思维导图总结 这里写目录标题队列的定义于性质如何实现队列的功能初始化队列入队列出队列队列的销毁队列取队头数据队列取队尾数据判断队列是否为空判断队列长度总结队列的定义于性质 队列是一种数据结构&#xff0c;他储存数据的方式就和排队一样 …

二十六、Kubernetes中Horizontal Pod Autoscaler(HPA)控制器详解

1、概述 在kubernetes中&#xff0c;有很多类型的pod控制器&#xff0c;每种都有自己的适合的场景&#xff0c;常见的有下面这些&#xff1a; ReplicationController&#xff1a;比较原始的pod控制器&#xff0c;已经被废弃&#xff0c;由ReplicaSet替代 ReplicaSet&#xff…

年终盘点(三)丨2022计讯物联团队不负韶华,奋力前行

光阴荏苒&#xff0c;时光悄然&#xff0c;成长的齿轮不断转动。2022年&#xff0c;计讯人在挑战中创造不凡&#xff0c;2023年&#xff0c;计讯人在希望中迎接新未来。 回首过去&#xff0c;计讯物联团队不断壮大&#xff0c;在奋勇前行中以坚持书写拼搏&#xff0c;在知难而…

记好这24个ES6方法,用于解决实际开发的JS问题

本文主要介绍24中es6方法&#xff0c;这些方法都挺实用的&#xff0c;本本请记好&#xff0c;时不时翻出来看看。 1.如何隐藏所有指定的元素 1 const hide (el) > Array.from(el).forEach(e > (e.style.display none)); 2 3 // 事例:隐藏页面上所有<img>元素? …

echarts——实现 面积图+柱状图+折线图等——基础积累

因为到年底了&#xff0c;很多项目组都开始做年终汇报&#xff0c;年终汇报的展示形式最常见的就是看板。 样式美观&#xff0c;可以放到电视机或者大屏上&#xff0c;通过图表的形式进行展示&#xff0c;简单明了&#xff0c;通俗易懂。 直接上最终效果图&#xff1a;是一个…

【C++】打开C++的大门

目录前言1.什么是C2.C的发展史3.C关键字&#xff08;C98&#xff09;4.命名空间4.1命名冲突4.2命名空间定义4.3命名空间使用5.输入输出6.缺省参数6.1缺省参数的概念6.2缺省参数分类7.函数重载7.1函数重载概念7.2C函数重载的原理——名字修改8.引用8.1引用的概念8.2引用特性8.3常…

ArcGIS基础实验操作100例--实验94计算栅格图层总和值

本实验专栏参考自汤国安教授《地理信息系统基础实验操作100例》一书 实验平台&#xff1a;ArcGIS 10.6 实验数据&#xff1a;请访问实验1&#xff08;传送门&#xff09; 空间分析篇--实验94 计算栅格图层总和值 目录 一、实验背景 二、实验数据 三、实验步骤 &#xff08;…

【观察】软硬件底层创新“开花结果”,亚马逊云科技的沉淀与释放

2006年&#xff0c;亚马逊云科技推出了Amazon Web Services&#xff0c;正式“开创”出了云计算市场。同年8月&#xff0c;Amazon Elastic Compute Cloud (EC2) 开放了 beta 测试&#xff0c;启动了云上计算的创新和革命。从此&#xff0c;亚马逊云科技在云计算软硬件底层技术创…

软件测试复习03:动态测试——黑盒测试

作者&#xff1a;非妃是公主 专栏&#xff1a;《软件测试》 个性签&#xff1a;顺境不惰&#xff0c;逆境不馁&#xff0c;以心制境&#xff0c;万事可成。——曾国藩 文章目录等价划分法边值分析法错误推测法因果图法示例习题等价划分法 等价类&#xff1a;一个几何&#xf…

阿里云 gradle maven配置中心地址

仓库名称阿里云仓库地址阿里云仓库地址(老版)源地址centralhttps://maven.aliyun.com/repository/centralhttps://maven.aliyun.com/nexus/content/repositories/centralhttps://repo1.maven.org/maven2/jcenterhttps://maven.aliyun.com/repository/publichttps://maven.aliyu…

dp(五) 最长公共子串

最长公共子串_牛客题霸_牛客网 描述 给定两个字符串str1和str2,输出两个字符串的最长公共子串 题目保证str1和str2的最长公共子串存在且唯一。 数据范围&#xff1a; 1≤∣str1∣,∣str2∣≤50001≤∣str1∣,∣str2∣≤5000 要求&#xff1a; 空间复杂度 O(n2)O(n2)&#x…

【阶段三】Python机器学习22篇:机器学习项目实战:GBDT分类模型

本篇的思维导图: 项目实战(GBDT分类模型) 项目背景 应用GBDT算法实现多分类模型,目标是实现GBDT多分类项目的全流程。 数据获取 本次建模数据来源于网络,数据项统计如下: 编号  变量名称 <

聚焦:XuperOS成长计划FAQ

1月12日&#xff0c;百度超级链发布XuperOS成长计划&#xff08;&#x1f449;XuperOS 新年致辞&#xff1a;创世、监督、共建、国际&#xff09;。以下是我们整理的关于成长计划的常见问题&#xff0c;为关心XuperOS的广大朋友答疑解惑。问&#xff1a;XuperChain除了发行这四…