C++11 线程库

C++11 线程库

  • 一.thread类
    • 1.介绍
      • 1.框架
      • 2.构造
      • 3.赋值
      • 4.join与joinable
      • 5.id和get_id
      • 6.this_thread命名空间
      • 7.yield
      • 8.演示
  • 二.锁类
    • 1.互斥锁
      • 1.介绍
      • 2.使用
        • 1.配合lambda来使用
        • 2.ref
    • 2.递归锁和时间锁
      • 1.递归锁介绍
      • 2.例子
      • 3.时间锁介绍
  • 三.RAII管理锁类
    • 1.lock_guard
      • 1.介绍
      • 2.使用
      • 3.好处与不足
    • 2.unique_lock
      • 1.介绍
        • 1.框架
        • 2.构造,赋值和析构
        • 3.release和mutex
        • 4.swap与operator bool,owns_lock
    • 3.lock_guard和unique_lock的区别与适用场景
  • 四.条件变量类
    • 1.框架
    • 2.condition_variable
      • 1.框架+构造+析构
      • 2.wait和notify
      • 3.使用
  • 五.两个线程交替打印奇偶数,要求t1先打印
    • 1.前提1 : 多个新线程之间谁先运行是不确定的
    • 2.前提2 : 临界区当中有修改临界资源的代码,那么整个临界区当中无论是对临界资源的读和写都要加锁
    • 1.如何保证t1先打印,t2后打印
    • 2.如何交替打印[ 阻塞等待 ]呢
    • 3.如何让一个线程不能连续打印呢?
    • 4.验证
  • 六.其他版本
  • 七.原子类
    • 1.互斥锁的缺点
    • 2.原子类的引出
    • 3.介绍
      • 1.构造,赋值,is_lock_free
      • 2.store,load和exchange
    • 3.修改操作
    • 4.使用
  • 八.CAS与无锁化编程的介绍
    • 1.CAS介绍
    • 2.问题1
    • 3.用一下之前先介绍接口
    • 4.用一下

C++11当中的线程库对Linux下的pthread库进行了封装,把C写的pthread库由面向过程改为了面向对象
提供了thread类,锁类,RAII管理锁的类,条件变量类,原子类,下面我们逐一介绍并使用

一.thread类

1.介绍

1.框架

在这里插入图片描述

2.构造

在这里插入图片描述
含参构造创建线程,创建之后线程就会立刻启动

3.赋值

在这里插入图片描述

4.join与joinable

主线程创建并启动了一个新线程,必须要join它,否则主线程退出时就会报错,所以千万不要忘了join
当然,如果不想join,那么需要detach它
在这里插入图片描述
因此当多线程跟异常跟信号结合起来…
在这里插入图片描述

5.id和get_id

在这里插入图片描述
这个id函数是能由主线程去调用啊,因为新线程执行的函数里面没有thread类型的对象啊,我要是想在新线程当中打印新线程自己的id该怎么办呢?
下面介绍this_thread命名空间的时候,就解决这一问题了
在这里插入图片描述
正因如此,调用的函数一般都是直接用void作为返回值类型

6.this_thread命名空间

在这里插入图片描述
因此新线程直接调用this_thread::get_id()即可获取自己的线程id

7.yield

yield : 让步
在这里插入图片描述

8.演示

在这里插入图片描述
在这里插入图片描述

二.锁类

在这里插入图片描述
下面我们先介绍锁和锁的方法

1.互斥锁

1.介绍

这里的互斥锁跟pthread库当中的非常像,一看大家就懂
有些场景下,获取锁的线程仍需要重入该函数,且重入之前依旧不能释放锁在这里插入图片描述

2.使用

1.配合lambda来使用

在这里插入图片描述

2.ref

在这里插入图片描述

ref可以在引用传参时保持引用属性

我们知道,可变参数模板的参数包是层层往下传的,传到最后一层的时候调用对应的构造函数来进行构造
又加上万能引用和完美转发之后,其中的过程非常复杂,我们可以暂时理解为(为了方便理解)参数包传递时,
由于万能引用+完美转发,导致传参时可能会生成引用对象的一个拷贝,因此我们必须要加ref来保持引用属性
否则就会编译报错

所以我们需要这么来写
在这里插入图片描述
或者这么写
在这里插入图片描述
建议以后用指针得了,C++11的报错真扛不住…

2.递归锁和时间锁

1.递归锁介绍

有了互斥锁为何还要有递归锁呢?
有些场景下,获取锁的线程仍需要重入该函数,且重入之前依旧不能释放锁,此时如果该锁是互斥锁,那么就会导致死锁

递归锁会记录加锁的次数,同一线程每次申请锁,计数+1,每次释放锁,计数减1

递归锁: 同一个线程可以多次申请同一把锁,而不会造成死锁,也叫做可重入锁

2.例子

一个线程执行线程函数的临界区时,调用了另一个函数,另一个函数当中在满足某些条件时还需要再调用线程函数,此时就可能会发生死锁问题

class A
{
public:
	static void func()
	{
		mtx.lock();
		task();
		mtx.unlock();
	}
	static void task()
	{
		if(x++ == 0)
			func();
	}
	static mutex mtx;
	static int x;
};

mutex A::mtx;
int A::x = 0;

int main()
{
	thread t1(&A::func);
	t1.join();
	return 0;
}

在这里插入图片描述
因此C++11线程库就搞出来递归锁
在这里插入图片描述
递归锁也是,禁掉了拷贝构造,移动构造,拷贝赋值,移动赋值

3.时间锁介绍

时间锁跟互斥锁差不多,只不过增加了try_lock_for和try_lock_until这两个函数
顾名思义,try_lock_for是尝试获取锁持续多长时间,try_lock_until是尝试获取锁到什么时候

递归时间锁就是递归锁+try_lock_for和try_lock_until

三.RAII管理锁类

1.lock_guard

1.介绍

在这里插入图片描述
lock_guard支持移动构造,但是没意义,
因为lock_guard就只是负责申请锁和释放锁的,而且lock_guard不支持默认构造,也就不能先构造lock_guard,
然后再让lock_guard管理某个对象(不支持默认构造可以看作是lock_guard的一个缺点)

lock_guard不支持拷贝赋值,也不支持移动赋值,因为lock_guard不支持默认构造,不能先构造lock_guard,然后再让lock_guard管理某个对象

2.使用

在这里插入图片描述

3.好处与不足

lock_guard的好处是:

  1. 利用RAII管理锁的申请与释放,无需我们手动申请释放锁了
  2. 使用起来简单,简洁

lock_guard的不足:

  1. 只支持管理互斥锁,无法管理递归锁,时间锁和递归时间锁
  2. 不支持手动释放锁,使用起来不灵活
  3. 不支持默认构造和移动赋值,因此无法将lock_guard的构造和对锁资源的管理分开,不够灵活

因此大佬又发明了一个unique_lock

2.unique_lock

1.介绍

1.框架

在这里插入图片描述
unique_lock可以管理各种类型的锁,互斥锁,递归锁,时间锁,递归时间锁

2.构造,赋值和析构

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

3.release和mutex

在这里插入图片描述

4.swap与operator bool,owns_lock

在这里插入图片描述

3.lock_guard和unique_lock的区别与适用场景

  1. 管理的锁的类型不同:
    lock_guard只能管理互斥锁
    而unique_lock能管理所有类型的锁, 使用起来更加通用

  2. 是否能像锁一样来使用的不同:
    lock_guard无法像锁一样来使用,unqiue_lock能够像锁一样来使用,因此unqiue_lock支持手动申请释放锁,更加灵活

  3. 是否支持对象随时跟锁相绑定与解除绑定的不同 :
    lock_guard跟锁是强绑定的,不支持随时进行绑定与解除绑定
    unique_lock跟锁是不强行绑定的,支持随时绑定与解除绑定(默认构造+移动赋值进行随时绑定,通过release来解除绑定恢复为默认构造的状态)

  4. 因为unique_lock能进行手动加锁与解锁,因此可以跟条件变量一起使用,而lock_guard无法进行手动加锁与解锁,因此无法跟条件变量一起使用
    (注意: lock_guard和unique_lock都能跟信号量一起配合使用)

  5. 总的来说,lock_guard更加简单且更易于控制,适用于管理互斥锁的比较简单的场景
    unique_lock更加灵活且复杂,适用于各种类型的锁和比较复杂的场景

关于使用,我们跟条件变量一起配合使用,所以下面介绍条件变量

四.条件变量类

1.框架

在这里插入图片描述

2.condition_variable

1.框架+构造+析构

在这里插入图片描述

2.wait和notify

在这里插入图片描述
当一个线程调用wait的时候需要传入自己的锁,调用时会将自己的锁解开在进行阻塞,这么设计是为了避免发生死锁问题
当一个线程被notify唤醒之后需要重新竞争锁,竞争到锁的线程才能继续执行,没有竞争到的会阻塞在申请锁的位置
(小心: 此时如果用if,可能会导致伪唤醒)

3.使用

模拟一下多线程抢票,一开始就100张票,两个线程抢,抢完之后主线程加100张票,新线程继续抢,往复循环
在这里插入图片描述
其实可以不用条件变量,但是不用的话会导致多线程频繁申请锁,判断ticket是否大于0,
然后释放锁,妥妥的浪费资源,因此才要用条件变量来阻塞它们,不让他们浪费CPU资源

尽管条件变量设计的初衷是为了实现线程同步,避免竞争锁能力弱的线程出现饥饿问题的
但是这里条件变量可以提高我们的效率(减少浪费就是提高效率)

五.两个线程交替打印奇偶数,要求t1先打印

这是一个非常经典的多线程题目,下面我们一起来探讨一下该如何做

我们就统一规定t1和t2有一个共享资源num,初始值为1
t1和t2先打印num,后++num

1.前提1 : 多个新线程之间谁先运行是不确定的

首先我们要先明确一点 : 创建多线程时,多个新线程之间谁先运行是不确定的,由OS调度时它说了算
在这里插入图片描述

2.前提2 : 临界区当中有修改临界资源的代码,那么整个临界区当中无论是对临界资源的读和写都要加锁

因为如果只对写操作进行加锁,那么一个线程就可能会在另一个线程写入操作之前或者之后读取数据,就会导致数据不一致问题
因此需要加锁

1.如何保证t1先打印,t2后打印

因此我们一定要想出一个方法让t1能够先打印,t2后打印
但是因为t1和t2谁先运行我们决定不了,有这么两种情况:

  1. t2先运行,此时t2不能进行打印,等到t1打印完成之后,才能开始打印
  2. t1先运行,然后t2才开始运行,此时t2需要直接打印,不能在t1下一次打印之后才打印

因此我们要能够在t2运行之后判断t1是否运行了,
如果t1还没运行,我需要阻塞等待
如果t1已经运行了,我直接运行即可

那如何判断呢?
因为我们要保证t1第一次运行只能让1变成2,因此对于t2来说,如果t1运行了,那么num就是偶数
如果t1还没运行.那么num就是奇数
在这里插入图片描述
因此t2的框架一定是这样的,同理,我们切换到t1的视角

t2打印2之前t1是不能打印3的,因此t1在打印3之前也需要知道t2是否把num改成了3,如何知道呢?
判断num是不是奇数即可

因此t1的框架一定是这样的:
在这里插入图片描述

2.如何交替打印[ 阻塞等待 ]呢

我们知道 想要让多线程访问修改临界资源具有顺序性,就需要用到条件变量(🔔)
又因为任意时刻,num一定不是奇数就是偶数,因此任意时刻一定有一个线程正在运行/等待被调度,而另一个线程正在阻塞/等待被调度

因此我们可以搞一个条件变量,t1和t2阻塞等待时就等待那个条件变量
t1和t2打印并++num之后唤醒另一个线程

凡是使用条件变量进行唤醒就需要考虑一个问题:
此时该在唤醒队列当中的线程还没有进行等待的话,我就进行唤醒,会有什么后果吗?

我们先写出代码来,然后在分析一下
在这里插入图片描述

3.如何让一个线程不能连续打印呢?

如果t1/t2连续打印,那么必定会导致它们既打印奇数又打印偶数,因此这种情况我们也要避免
t1运行完之后,num一定是偶数,而t1的阻塞条件又恰好是偶数,所以t1本来就不能连续打印,
同理t2也是如此,因此我们之前的处理是一箭双雕

4.验证

至此,我们已经实现了:

  1. 让t1先打印
  2. 不让一个线程连续打印
  3. 实现交替打印/阻塞等待+通知

仔细想一下,所有的要求都已经满足了啊,没错,这个题已经做完了…此时我们直接运行
在这里插入图片描述
验证完毕

六.其他版本

刚才那个版本不具有普适性,因为如果我需求变了
t1和t2交替打印正负数怎么办?
while(num%2==0)这个就需要变,可是我不想变,怎么办?

我们可以搞一个bool类型的标记位,具体打印修改逻辑你肯定要变,但是我们能够保证甭管你打印啥
一定是t1先打印,然后t1,t2交替打印
在这里插入图片描述

int main()
{
	int num = 1;
	condition_variable cv;
	mutex mtx;
	bool flag = false;//false时: t2阻塞  true时: t1阻塞

	auto callback_func = [&num]() {cout << this_thread::get_id() << "  " << num++ << endl; };


	thread t1([&]() {
		while (true)
		{
			unique_lock<mutex> ulock(mtx);
			while (flag == true)
			{
				//阻塞等待 TODO
				cv.wait(ulock);
			}
			//等待成功 或者 压根无需等待
			callback_func();
			flag = true;//让我自己阻塞
			cv.notify_one();
		}
		});

	thread t2([&]() {
		while (true)
		{
			unique_lock<mutex> ulock(mtx);
			while (flag == false)
			{
				//阻塞等待 TODO
				cv.wait(ulock);
			}
			//等待成功 或者 压根无需等待
			callback_func();
			flag = false;
			cv.notify_one();
		}
		});

	cout << "t1.get_id():  " << t1.get_id() << endl;
	cout << "t2.get_id():  " << t2.get_id() << endl;
	t1.join();
	t2.join();
	return 0;
}

七.原子类

多线程需要保护临界资源,本质是因为临界资源的修改操作不是原子的,如果临界资源修改的操作是原子的,也就不需要加锁了

1.互斥锁的缺点

因为加锁会导致多线程访问临界资源会由并发执行变为串行执行从而影响效率,
而且如果修改临界资源比较快的话,会导致多线程频繁被OS调度,往返于内核态和用户态之间,从而降低效率,浪费资源
因此有大佬提出了自旋锁,而且此时yield也可以用上了
但是加锁毕竟不好,影响多线程的并发,因此有大佬研究了CAS与无锁化编程

2.原子类的引出

发明了原子类,提供了对单个数据类型的原子操作,可以用于一些内置类型:整型家族,bool,指针,枚举

(注意: C++11不支持double等浮点数,之后的版本可能支持了,但是并不广泛和通用)

对于自定义类型,必须满足以下条件:

它的大小和内存布局是固定的,并且在所有平台上都是一致的
它的操作(如赋值、比较等)必须是原子的

因此原子类更常用于内置类型,特别是整型家族

3.介绍

1.构造,赋值,is_lock_free

在这里插入图片描述
在这里插入图片描述

2.store,load和exchange

在这里插入图片描述
在这里插入图片描述

3.修改操作

最常用的还是修改操作
在这里插入图片描述

4.使用

其实原子类就只是能够保证单个操作的原子性而已,因此只有在需要保证单个操作的原子性时,原子类才有比较好的用武之地
当涉及到多个需要一起执行的原子操作时,(比如: 判断+修改),还是要用锁的啊

单个操作: 无需加锁,用原子类即可
在这里插入图片描述
复合操作: 单纯的原子类的简单操作是无法保证的,还是要加锁
在这里插入图片描述
VS2019下验证半天看不出来,跑Linux下一跑就验证出来了

难道原子类真的只有这些用处???

atomic还有两个函数可以帮助我们抛弃锁,这两个函数底层采用的是CAS操作,用于实现无锁化编程
下面我们单独搞一个标题来介绍

八.CAS与无锁化编程的介绍

C++11的原子类当中有两个函数可以用于CAS操作
atomic_compare_exchange_weak和atomic_compare_exchange_strong

陈皓大佬的文章: 无锁队列的实现
我们就看着大佬的文章看一下CAS吧.至于无锁队列,陈皓大佬介绍的非常详细,大家感兴趣的话看一下就行了

我们重点在了解,学习,掌握基础的CAS使用

1.CAS介绍

在这里插入图片描述
如果失败,不做任何操作,整个比较并交换的操作是一个原子操作,不可被中断.

我们现在可能有两个问题:

  1. 为何这么做就能够解决数据不一致问题?
  2. 能否用一下呢?

2.问题1

在这里插入图片描述
而CAS就是将线程硬件上下文当中的值作为期望值/旧值 跟 内存当中的值进行比较
只有相等时才更新内存当中的值为修改后的新值,因此CAS “迫使” 线程必须重新读取对比内存当中的值,如果不一致,不允许修改

3.用一下之前先介绍接口

在这里插入图片描述
失败时利用内存当中的值更新旧值
因为一出错就返回false,因此我们可以配合do while循环,我们就用weak了,反正咱有while

do{}while(!CAS);

4.用一下

有了CAS做加持,原子类才能够挺起腰板来…
我们的抢票完全不需要锁了
在这里插入图片描述

atomic<int> ticket(10000);
void g()
{
	bool ok = true;
	while (ok)
	{
		int oldval = ticket.load(), newval = oldval - 1;//先取出旧值,搞一个新值
		if (oldval > 0)
		{
			do
			{
				newval = oldval - 1;
				if (oldval <= 0)
				{
					ok = false;
					break;
				}
				//返回false会自动更新oldval,因此我只需要更新newval即可
			}while (!atomic_compare_exchange_weak(&ticket, &oldval, newval));//旧值 和 新值
			if(ok)
			{
				cout << this_thread::get_id() << "# ticket #  " << oldval << endl;//打印旧值
			}
		}
		else ok = false;
	}
}


int main()
{
	thread t1(g);
	thread t2(g);
	thread t3(g);
	thread t4(g);
	t1.join();
	t2.join();
	t4.join();
	t3.join();
	return 0;
}

以上就是C++11 线程库的全部内容,希望能对大家有所帮助!!

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

AOP总结

AOP是什么 AOP是面向切面编程&#xff0c;其目的是将横切关注点从核心业务代码中分离出来&#xff0c;通过动态代理等方式&#xff0c;实现代码的增强和解耦&#xff0c;使得其具有更好的可维护性和可扩展性。 其中横切关注点是多个类或对象的公共行为&#xff0c;如事务管理…

五种独立成分分析(ICA)

代码原理及流程 代码实现了混合信号的独立成分分析&#xff08;ICA&#xff09;过程&#xff0c;主要包括以下几个步骤&#xff1a; 原始语音信号读取与显示&#xff1a;首先读入原始的两个语音信号(music.wav和man.wav)&#xff0c;并显示在图中的第一和第二个子图中。混合声…

mfc140.dll丢失原因和mfc140.dll丢失修复办法分享

mfc140.dll是与微软基础类库&#xff08;Microsoft Foundation Classes, MFC&#xff09;紧密相关的动态链接库&#xff08;DLL&#xff09;文件。MFC是微软为C开发者设计的一个应用程序框架&#xff0c;用于简化Windows应用程序的开发工作。以下是mfc140.dll文件的一些关键属性…

项目管理:敏捷实践框架

一、初识敏捷 什么是敏捷(Agile)?敏捷是思维方式。 传统开发模型 央企,国企50%-60%需求分析。整体是由文档控制的过程管理。 传统软件开发面临的问题: 交付周期长:3-6个月甚至更长沟通效果差:文档化沟通不及时按时发布低:技术债增多无法发版团队士气弱:死亡行军不关注…

如何安装虚拟机Wmware,并且在虚拟机中使用centos系统

1. 前言 大家好&#xff0c;我是jiaoxingk 本篇文章主要讲解如何安装虚拟机&#xff0c;并且在虚拟机中安装centos系统&#xff0c;让windows电脑也能够使用Linux系统 2. 虚拟机的介绍 在安装Vmware之前&#xff0c;我们先做虚拟机的介绍 虚拟机&#xff1a;通过软件虚拟出来的…

20240523每日运维--------聊聊docker简介(一)

dotCloud 说Docker&#xff0c;必不可免不得不说dotCloud&#xff0c;Docker本来只是dotCloud公司的内部项目&#xff0c;其公司创始人 Solomon Hykes 发了一个内部项目&#xff0c;而这个项目就是Docker&#xff0c;自从2013年docker开源以后&#xff0c;在世界范围引起相当轰…

服务器安全审计: chkrootkit 和 rkhunter 详解

chkrootkit 和 rkhunter 是两个广泛使用的安全工具,用于检测系统是否被Rootkit或其他恶意软件感染。本文将详细说明这两个工具的使用方法及如何解释检测结果。 1. chkrootkit 1.1. 安装 chkrootkit 在CentOS上安装 chkrootkit 可以使用以下命令: yum install chkrootkit -…

十四天学会Vue——Vue核心(理论+实战)(第一天)上篇

&#xff01;&#xff01;&#xff01;声明必看&#xff1a;由于本篇开始就写了Vue&#xff0c;内容过多&#xff0c;本篇部分内容还有待完善&#xff0c;小编先去将连续更新的js高阶第四天完成~本篇部分待完善内容明日更新 一、Vue核心&#xff08;上篇&#xff09; 热身top…

【机器学习300问】97、机器学习中哪些是凸优化问题,哪些是非凸优化问题?

在机器学习的领域中&#xff0c;多数模型的参数估计问题实质上可以转化为优化问题。鉴于机器学习模型的多样性&#xff0c;不同的模型会对应着不同的损失函数&#xff0c;进而形成各具特色的优化问题。了解优化问题的形式和特点&#xff0c;对于提升我们求解模型参数的效率和准…

Meta发布Chameleon模型预览,挑战多模态AI前沿

每周跟踪AI热点新闻动向和震撼发展 想要探索生成式人工智能的前沿进展吗&#xff1f;订阅我们的简报&#xff0c;深入解析最新的技术突破、实际应用案例和未来的趋势。与全球数同行一同&#xff0c;从行业内部的深度分析和实用指南中受益。不要错过这个机会&#xff0c;成为AI领…

(已开源-ICRA2023) High Resolution Point Clouds from mmWave Radar

本文提出了一种用于生成高分辨率毫米波雷达点云的方法&#xff1a;RadarHD&#xff0c;端到端的神经网络&#xff0c;用于从低分辨率雷达构建类似激光雷达的点云。本文通过在大量原始雷达数据上训练 RadarHD 模型&#xff0c;同时这些雷达数据有对应配对的激光雷达点云数据。本…

【C++课程学习】:命名空间的理解(图文详解)

&#x1f381;个人主页&#xff1a;我们的五年 &#x1f50d;系列专栏&#xff1a;C课程学习 &#x1f389;欢迎大家点赞&#x1f44d;评论&#x1f4dd;收藏⭐文章 目录 &#x1f4f7;1.命名冲突 &#x1f4f7;2.重定义 &#x1f4f7;3.命名空间 &#x1f37a;命名空间可…

内存函数详解,包含部分字符串函数

目录 一&#xff0c;memcpy内存函数的介绍 二memmove函数的介绍 三&#xff0c;memset的函数使用 四&#xff0c;memcmp的介绍 五&#xff0c;内存函数的模拟实现&#xff0c;以及一个字符串函数strstr的模拟实现 5.1memcpy函数的实现 5.2memmove的模拟实现 5.3memcmp的模拟…

自定义原生小程序顶部及获取胶囊信息

需求&#xff1a;我需要将某个文字或者按钮放置在小程序顶部位置 思路&#xff1a;根据获取到的顶部信息来定义我需要放的这个元素样式 * 这里我是定义某个指定页面 json&#xff1a;给指定页面的json中添加自定义设置 "navigationStyle": "custom" JS&am…

计算机毕业设计 | springboot+vue房屋租赁管理系统(附源码)

1&#xff0c;绪论 1.1 课题来源 随着社会的不断发展以及大家生活水平的提高&#xff0c;越来越多的年轻人选择在大城市发展。在大城市发展就意味着要在外面有一处安身的地方。在租房的过程中&#xff0c;大家也面临着各种各样的问题&#xff0c;比如需要费时费力去现场看房&…

模型蒸馏笔记

文章目录 一、什么是模型蒸馏二、如何蒸馏三、实践四、参考文献 一、什么是模型蒸馏 Hinton在NIPS2014提出了知识蒸馏&#xff08;Knowledge Distillation&#xff09;的概念&#xff0c;旨在把一个大模型或者多个模型ensemble学到的知识迁移到另一个轻量级单模型上&#xff0…

【数据结构】二叉树的认识与实现

目录 二叉树的概念&#xff1a; 二叉树的应用与实现&#xff1a; 二叉树实现接口&#xff1a; 通过前序遍历的数组"ABD##E#H##CF##G##"构建二叉树 二叉树节点个数​编辑 二叉树叶子节点个数 二叉树第k层节点个数 二叉树查找值为x的节点​编辑 二叉树前序遍…

CCF20231201——仓库规划

CCF20231201——仓库规划 代码如下&#xff1a; #include<bits/stdc.h> using namespace std; int main() {int n,m,a[1001][11],b[1001]{0};cin>>n>>m;for(int i1;i<n;i){for(int j1;j<m;j)cin>>a[i][j];}for(int i1;i<n;i){bool foundfals…

分享免费的手机清理软件app,一款国外开发的手机清理神器,让手机再战两年!

手机内存越来越大&#xff0c;软件却越来越占地方&#xff0c;就像微信这家伙&#xff0c;轻轻松松就吃了十几个G&#xff01; 害得阿星8128G的手机&#xff0c;本来想换新的&#xff0c;结果用了这款Avast Cleanup软件&#xff0c;瞬间感觉手机还能再战两年&#xff01; 注意…

2024年云南特岗教师报名流程,超详细,明天就开始报名哦!

2024年云南特岗教师报名流程&#xff0c;超详细&#xff0c;明天就开始报名哦&#xff01;