【C++11】多线程

news2024/11/24 0:59:08

      • 多线程
        • 创建线程
        • thread提供的成员函数
        • 获取线程id的方式
        • 线程函数参数的问题
        • 线程join场景和detach
      • 互斥量库(mutex)
        • mutex
        • recursive_mutex
        • lock_guard 和 unique_lock
      • 原子性操作库(atomic)
      • 条件变量库(condition_varuable)
      • 综合案例(实现两个线程交替打印1-100)

多线程

在C++11之前,涉及到多线程问题,都是和平台相关的,比如Windows和Linux下各有自己的接口,这使得代码的可移植性比较差。C++11中最重要的特性就是对线程进行了支持,使得C++在并行编程时不需要依赖第三方库,而且在原子操作中还引入了原子类的概念

创建线程

在这里插入图片描述

  1. 调用无参的构造函数创建

thread提供了无参的构造函数,调用无参的构造函数创建出来的线程对象没有关联任何的线程函数对象,也没有启动任何线程。

thread t1;
#include <iostream>
#include <thread>
using namespace std;

void func(int n)
{
	cout << n << endl;
}

int main()
{
	thread t1;
	t1 = thread(func, 10);
	t1.join();
	return 0;
}

我们的thread是提供了移动赋值函数的,所以,当后序需要让该线程关联线程函数的时候,我们可以定义一个匿名的线程,然后调用移动赋值传给他
在这里插入图片描述
thread类是防拷贝的,不允许拷贝构造和拷贝赋值,但是可以移动构造和移动赋值,可以将一个线程对象关联线程的状态转移给其他线程对象,并且转移期间不影响线程的执行。

  1. 调用带参的构造函数

thread的带参构造函数的定义如下:

template <class Fn, class... Args>
explicit thread (Fn&& fn, Args&&... args);

参数说明:

  • fn:可调用对象,比如:仿函数,指针,lambda表达式,被包装器包装后的可调用对象等。
  • args:进行对fn进行传值。

调用带参的构造函数创建线程对象,能够将线程对象与线程函数fn进行关联。比如:

void Func(int n, int num)
{
	for (int i = 0; i < n; i++)
	{
		cout <<num<<":" << i << endl;
	}
	cout << endl;
}

int main()
{
	thread t2(Func, 10,666);
	
	t2.join();
	return 0;
}

在这里插入图片描述

结合之前的lambda函数,这么我们可以很明显的看到lambda的作用

int main()
{
	//thread t2(Func, 10,666);

	thread t2([](int n = 10, int num = 666)
		{
			for (int i = 0; i < n; i++)
			{
				cout << num << ":" << i << endl;
			}
			cout << endl;
		});
	
	t2.join();
	return 0;
}

其输出结果和上图是一样的,注意,这么线程的形参只有一个仿函数lambda

使用一个容器来保存线程

#include <iostream>
#include <thread>
#include <vector>
using namespace std;
int main()
{
	size_t m;//线程个数
	cin >> m;

	vector<thread> vthread(m);//直接初始化长度为10
	
	for (int i = 0; i < m; ++i)
	{

		vthread[i] = thread([i]()
			{
				cout << "我是第" << i << "个线程" << endl;
			});
	}
	
	for (auto& e : vthread)
	{
		e.join();
	}
	return 0;
}

在这里插入图片描述

thread提供的成员函数

在我们多线程中,常用的成员函数如下:

  • join:对该线程进行等待,在等待的线程返回之前,调用join将会将线程进行阻塞,我们主要阻塞的是主线程。
  • joinable:判断该线程是否已经执行完毕,如果是则返回true,否则返回false。
  • 在这里插入图片描述
  • detach:将该线程进行分离主线程,被分离后,不在需要创建线程的主线程调用join进行对其等待
  • get_id:获取该线程的id

此外,joinable函数还可以用于判定线程是否是有效的,如果是以下任意情况,则线程无效:

  • 采用无参构造函数构造的线程对象。(该线程对象没有关联任何线程)
  • 线程对象的状态已经转移给其他线程对象。(已经将线程交给其他线程对象管理)
  • 线程已经调用join或detach结束。(线程已经结束)
获取线程id的方式

其实调用线程id的方法有两种,实际情况我们看下边的代码:

#include <iostream>
#include <thread>
#include <vector>
using namespace std;
int main()
{
	size_t m;//线程个数
	cin >> m;

	vector<thread> vthread(m);//直接初始化长度为10
	
	for (int i = 0; i < m; ++i)
	{

		vthread[i] = thread([i]()
			{
				printf("我是第%d个线程\n", i);
				cout << this_thread::get_id() << endl;
			});
	}

	for (auto& e : vthread)
	{
		cout << e.get_id() << endl;
	}
	for (auto& e : vthread)
	{
		e.join();
	}

	return 0;
}

在这里插入图片描述

  • 两者调用ID的环境是不同的
  • 从代码中我们可以看到get_id是需要线程对象来调用的
  • 但是this_thread::get_id我们通过多线程提供的特殊窗口可以不通过线程对象就可以直接调用

this_thread命名空间中还提供了以下三个函数:

  • yield:当前线程“放弃”执行,让操作系统调度另一线程继续执行
  • sleep_until :让当前线程休眠到一个具体时间点
  • sleep_for:让当前线程休眠一个时间段
线程函数参数的问题

线程函数的参数是以值拷贝的方式拷贝到线程栈空间中的,就算线程函数的参数为引用类型,在线程函数中修改后也不会影响到外部实参,因为其实际引用的是线程栈中的拷贝,而不是外部实参。比如:

void add(int& num)
{
	num++;
}
int main()
{
	int num = 0;
	thread t(add, num);
	t.join();

	cout << num << endl; //0
	return 0;
}

解决其办法有三种:

  • 方式一:借助std::ref函数

  • 在这里插入图片描述

  • 方式二:地址的拷贝

  • 在这里插入图片描述

  • 方式三:借助lambda表达式

  • 在这里插入图片描述

线程join场景和detach

当启动线程后,如果不使用join进行阻塞等待的话程序就会直接报错,因为此时存在内存泄漏问题。

thread库给我们提供了一共两种回收线程的方法

join方式

使用join进行线程回收时,一个线程只能回收一个,如果进行多次回收就会直接报错

但是如果你对一个线程回收后,又对此线程再一次的进行了移动赋值,那么此时还可以再一次的对线程进行二次join,如下代码案例:

void func(int n = 20)
{
	cout << n << endl;
}

int main()
{
	thread t(func, 20);
	t.join();

	t = thread([](int num = 10) {cout << num << endl; });
	t.join();
}

在这里插入图片描述
需要注意的是,如果线程运行起来后,线程的内部发生了异常,那么我们锁设置的阻塞就起不到任何作用了,

detach方式

主线程创建新线程后,也可以调用detach进行将线程和主线程分离,分离后,新线程会到后台运行,其所有权和控制权将会交给C++运行库,此时,c++运行库会保证当线程退出时,其相关资源能够被正确回收

#include <mutex>
#include <iostream>
#include <thread>
int x = 0;
mutex mtx;
void threadFunc(int n)
{
    mtx.lock();
    for (int i = 0; i < 100000; i++)
    {
        x++;
    }

    std::cout << "Hello from detached thread:" << n << std::endl;
    mtx.unlock();
}

int main()
{
    std::thread t1(threadFunc,11111);
    std::thread t2(threadFunc,22222);
    t1.detach();  // 将线程设置为可分离的
    t2.detach();  // 将线程设置为可分离的
    // 主线程继续执行其他任务
    std::cout << "Main thread continues..." << std::endl;

    // 不要忘记在主线程结束前等待一段时间,以免分离的线程还未执行完
    std::this_thread::sleep_for(std::chrono::seconds(1));
    cout << x << endl;
    return 0;
}

在这里插入图片描述

互斥量库(mutex)

在我们的c++11中,我们一共提供了四种互斥锁形式

mutex

1. std::mutex
mutex锁是C++11提供的最基本的互斥量,mutex对象之间不可以进行拷贝,也不能进行移动。

mutex中常用的成员函数如下:

  • lock:对互斥量进行加锁
  • unlock:对互斥量进行解锁,释放互斥量所有权
  • try_lock:尝试对互斥量进行加锁。

线程函数调用lock时,可能会发生三种情况:

  • 如果互斥量当前没有被其他线程锁住,则调用线程将该互斥量进行加锁,知道调用 unlock后,才对其进行解锁。
  • 如果进行加锁时,发现已经被其他线程已经加过锁了,此时会进入阻塞状态。
  • 如果该互斥量被当前调用线程锁住,则会产生死锁

线程函数调用try_lock时,可能会发生三种情况:

  • 如果互斥量当前没有被其他线程锁住,则调用线程将该互斥量进行加锁,知道调用 unlock后,才对其进行解锁。
  • 如果进行加锁时,发现已经被其他线程已经加过锁了,此时会返回false,并不会对其进行阻塞。
  • 如果该互斥量被当前调用线程锁住,则会产生死锁

在没有对临界资源加锁的时候,由于是多个进程同时进行,这时,不能同步的,正确的完成我们的任务,此时我们就需要给临界资源进行加锁

在这里插入图片描述

正确做法

#include <mutex>
#include <iostream>
#include <thread>
int x = 0;
mutex mtx;
void threadFunc(int n)
{
    mtx.lock();
    for (int i = 0; i < 100000; i++)
    {
        x++;
    }

    std::cout << "Hello from detached thread:" << n << std::endl;
    mtx.unlock();
}

int main()
{
    std::thread t1(threadFunc,11111);
    std::thread t2(threadFunc,22222);
    t1.detach();  // 将线程设置为可分离的
    t2.detach();  // 将线程设置为可分离的
    // 主线程继续执行其他任务
    std::cout << "Main thread continues..." << std::endl;

    // 不要忘记在主线程结束前等待一段时间,以免分离的线程还未执行完
    std::this_thread::sleep_for(std::chrono::seconds(1));
    cout << x << endl;
    return 0;
}

在这里插入图片描述

recursive_mutex

2. std::recursive_mutex
recursive_mutex叫做递归互斥锁,该锁专门用于递归函数中的加锁操作。

  • 有一些线程避免不了它是递归函数,但是如果对递归里的临界资源进行加锁时,可能会持续申请自己还还未释放的锁,进而导致死锁问题。
  • 而recursive_mutex允许同一个线程对互斥量进行多次上锁(即递归上锁),来获得互斥量对象的多层所有权,但是释放互斥量时需要调用与该锁层次深度相同次数的unlock。
int x = 0;
recursive_mutex mtx;

void Func(int n)
{
	if (n == 0)
		return;

	//递归锁的原理就是当调用自己本身时,发现给自己上锁的还是自己,这时会自动解锁,通过此种方法来进行重复加锁解锁
	mtx.lock();
	++x;

	Func(n - 1);

	mtx.unlock();
}

int main()
{
	thread t1(Func, 1000);
	thread t2(Func, 2000);

	t1.join();
	t2.join();

	cout << x << endl;

	return 0;
}

在这里插入图片描述
后两种可以查看收藏

lock_guard 和 unique_lock

为什么要使用lock_guard 和 unique_lock呢?

在我们平时使用锁中,如果锁的范围比较大,那么极度有可能在中途时忘记解锁,此后申请这个锁的线程就会被阻塞住,也就造成了死锁的问题,例如:

mutex mtx;
void func()
{
	mtx.lock();
	//...
	FILE* fout = fopen("data.txt", "r");
	if (fout == nullptr)
	{
		//...
		return; //中途返回(未解锁)
	}
	//...
	mtx.unlock();
}
int main()
{
	func();
	return 0;
}

因此使用互斥锁时如果控制不好就会造成死锁,最常见的就是此处在锁中间代码返回,此外还有一个比较常见的情况就是在锁的范围内抛异常,也很容易导致死锁问题。

因此C++11采用RAII的方式对锁进行了封装,于是就出现了lock_guard和unique_lock。

为此c++11推出了解决方案,采用RAII的方式对锁进行了封装,于是就出现了lock_guard和unique_lock。

lock_guard

lock_guard是C++11中的一个模板类,其定义如下:

template <class Mutex>
class lock_guard;

通过这种构造对象时加锁,析构对象时自动解锁的方式就有效的避免了死锁问题。比如:

mutex mtx;
void func()
{
	lock_guard<mutex> lg(mtx); //调用构造函数加锁
	//...
	FILE* fout = fopen("data.txt", "r");
	if (fout == nullptr)
	{
		//...
		return; //调用析构函数解锁
	}
	//...
} //调用析构函数解锁
int main()
{
	func();
	return 0;
}

模拟实现lock_guard

模拟实现lock_guard类的步骤如下:

  • lock_guard类中包含一个锁成员变量(引用类型),这个锁就是每个lock_guard对象管理的互斥锁。
  • 调用lock_guard的构造函数时需要传入一个被管理互斥锁,用该互斥锁来初始化锁成员变量后,调用互斥锁的lock函数进行加锁。
  • lock_guard的析构函数中调用互斥锁的unlock进行解锁。
  • 需要删除lock_guard类的拷贝构造和拷贝赋值,因为lock_guard类中的锁成员变量本身也是不支持拷贝的
class Lock_guard
{
public:
	Lock_guard(mutex& mtx)
		:_mtx(mtx)
	{
		_mtx.lock(); //加锁
	}
	~Lock_guard()
	{
		_mtx.unlock();//解锁
	}
	Lock_guard(const Lock_guard&) = delete;
	Lock_guard& operator=(const Lock_guard&) = delete;
private:
	mutex& _mtx;
};

mutex mtx;
int x;
int main()
{
	thread t1([]()
		{
			Lock_guard lg(mtx);
			x = 1;
		});
	t1.join();
	cout << x << endl;
}

在这里插入图片描述

unique_lock

比起lock_guard来说,unique_lock的逻辑和lock_guard的逻辑是一样的,都是调用类时加锁,出作用域时解锁,但是unique_lock比起lock_guard多加了几个条件,分别是:

  • 加锁/解锁操作:lock、try_lock、try_lock_for、try_lock_until和unlock。
  • 修改操作:移动赋值、swap、release(返回它所管理的互斥量对象的指针,并释放所有权)。
  • 获取属性:owns_lock(返回当前对象是否上了锁)、operator bool(与owns_lock的功能相同)、mutex(返回当前unique_lock所管理的互斥量的指针)。

具体实现逻辑,我们看下边的交替打印100的案例。

原子性操作库(atomic)

在上面的加锁案例中,多个线程同时对全局变量x进行++操作,并且,我们还对此操作进行了加锁,主要原因时++操作不是原子的,原子究竟是个什么呢?
原子大概的来说只有两个状态,一种是在运行,一种是不在运行,当处于在运行状态时,当其他进程进来时,发现为在运行状态就会进行等待, 知道状态模式换了才会进入下一个线程。

在这里插入图片描述

int main()
{
	int n = 100000;
	atomic<int> x = 0;
	//atomic<int> x = {0};
	//atomic<int> x{0};
	//int x = 0;

	mutex mtx;
	size_t begin = clock();

	thread t1([&, n](){
			for (int i = 0; i < n; i++)
			{
				++x;
			}
		});

	thread t2([&, n]() {
			for (int i = 0; i < n; i++)
			{
				++x;
			}
		});

	t1.join();
	t2.join();

	cout << x << endl;
	return 0;
}

在这里插入图片描述

条件变量库(condition_varuable)

condition_variable中提供的成员函数,可分为wait系列和notify系列两类。

wait系列成员函数(wait自带解锁功能和阻塞功能)

wait系列成员函数的作用就是让调用线程进行排队阻塞等待,包括waitwait_forwait_until

下面先以wait为例子,wait函数提供了两个不同版本的接口:

//版本一
void wait(unique_lock<mutex>& lck);
//版本二
template<class Predicate>
void wait(unique_lock<mutex>& lck, Predicate pred);

函数说明:

  • 调用第一个版本的时候,需要传入的参数只有一个互斥锁,线程调用wait后,就会进入阻塞状态,直到被唤醒
  • 调用第二个版本的时候,不仅要传入一个互斥锁,还需要传入一个返回值类型为bool的可调用对象,与第一个版本的wait不同的是,当线程被唤醒后还需要调用传入的可调用对象,如果可调用对象的返回值为false,那么该线程还需要继续被阻塞。

实际wait具有两个功能

  • 一个是让线程在条件不满足时进行阻塞等待,另一个是让线程将对应的互斥锁进行解锁。
  • 当线程被阻塞时这个互斥锁会被自动解锁,而当这个线程被唤醒时,又会自动获得这个互斥锁。

notify系列成员函数

notify系列成员函数的作用就是唤醒等待的线程,包括notify_one和notify_all。

  • notify_one:唤醒等待队列中的首个线程,如果等待队列为空则什么也不做。
  • notify_all:唤醒等待队列中的所有线程,如果等待队列为空则什么也不做。

综合案例(实现两个线程交替打印1-100)

尝试用两个线程交替打印1-100的数字,要求一个线程打印奇数,另一个线程打印偶数,并且打印数字从小到大依次递增。

该题目主要考察的就是线程的同步和互斥。

#include<mutex>
#include<condition_variable>

int main()
{
	mutex mtx;
	condition_variable cv;

	int n = 100;
	int x = 1;

	// 问题1:如何保证t1先运行,t2阻塞?
	// 问题2:如何防止一个线程不断运行?
	thread t1([&, n]() {
		while (1)
		{
			unique_lock<mutex> lock(mtx);
			if (x >= 100)
				break;

			//if (x % 2 == 0) // 偶数就阻塞
			//{
			//	cv.wait(lock);
			//}
			cv.wait(lock, [&x]() {return x % 2 != 0; });

			cout << this_thread::get_id() << ":" << x << endl;
			++x;

			cv.notify_one();
		}
		});

	thread t2([&, n]() {
		while (1)
		{
			unique_lock<mutex> lock(mtx);
			if (x > 100)
				break;

			//if (x % 2 != 0) // 奇数就阻塞
			//{
			//	cv.wait(lock);
			//}
			cv.wait(lock, [&x](){return x % 2 == 0; });


			cout << this_thread::get_id() << ":" << x << endl;
			++x;

			cv.notify_one();
		}
		});

	t1.join();
	t2.join();

	return 0;
}

在这里插入图片描述

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

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

相关文章

Linux 回收内存到底怎么计算anon/file回收比例,只是swappiness这么简单?

概述 Linux内核为了区分冷热内存,将page以链表的形式保存,主要分为5个链表,除去evictable,我们主要关注另外四个链表:active file/inactive file,active anon和inactive anon链表,可以看到这主要分为两类,file和anon page,内存紧张的时候,内核开始从inactive tail定…

上网Tips: Linux截取动态效果图工具_byzanz

链接1 链接2 安装&#xff1a; sudo apt-get install byzanz 查看指令 说明 byzanz-record --help日常操作 xwininfo点击 待录制窗口 左上角 byzanz-record -x 72 -y 64 -w 1848 -h 893 -d 10 --delay5 -c /home/xixi/myGIF/test.gif小工具 获取鼠标坐标 xdotool getm…

windows下python开发环境的搭建 python入门系列 【环境搭建篇】

在正式学习Python之前要先搭建Python开发环境。由于Python是跨平台的&#xff0c;所以可以在多个操作系统上进行编程 一、python的下载安装与配置 1、Python解释器 1. 要进行Python开发&#xff0c;首先需要Python解释器&#xff0c;这里说的安装Python就是安装Python解释器…

测试必备 | 测试工程师必知的Linux命令有哪些?

在日常的测试工作中&#xff0c;涉及到测试环境搭建及通过查看日志来定位相关问题时经常会用到Linux&#xff0c;在测试工程师的面试中也经常会有笔试或面试的题目来考查测试人员对Linux的熟悉程度&#xff0c;这里分享下测试工程师需知的 Linux 命令有哪些。 Linux 作为一种常…

一文带你全面解析postman工具的使用

写在前面&#xff1a;本文转自今日头条作者雨滴测试&#xff0c;感兴趣可点击下方链接查看原文 基础篇效率篇高级篇 一文带你全面解析postman工具的使用 文章目录 一文带你全面解析postman工具的使用基础篇一、postman安装说明1.下载与安装2.界面导航说明3.发送第一个请求 二、…

动态内存操作(2)

接上一篇文章http://t.csdn.cn/1ONDq&#xff0c;这次我们继续讲解关于动态内存的相关知识。 一、常见的动态内存错误 1.对NULL指针进行解引用操作 #include<stdio.h> #include<stdlib.h> #include<limits.h> int main() {int* p (int*)malloc(INT_MAX/4);…

Linux编译器-gcc/g++使用和动静态库的对比

目录 1. 背景知识 2.安装g/gcc 2.1安装指令 2.2安装成功查看指令 3gcc如何完成 3.1预处理(进行宏替换) 3.2编译&#xff08;生成汇编&#xff09; 3.3汇编&#xff08;生成机器可识别代码&#xff09; 3.4连接&#xff08;生成可执行文件或库文件&#xff09; 3.5gcc选…

使用Python爬虫抓取网站资源的方法

Python爬虫是一种自动化程序&#xff0c;用于从互联网上获取数据。使用Python爬虫可以轻松地抓取网站上的各种资源&#xff0c;例如文本、图片、视频等。在本文中&#xff0c;我们将介绍如何使用Python爬虫抓取网站资源。 安装Python 在使用Python爬虫之前&#xff0c;需要先安…

Socket网络编程练习题三:客户端上传文件到服务器

题目 客户端&#xff1a;将本地文件上传到服务器&#xff0c;接收服务器的反馈服务端&#xff1a;接收客户端上传的文件&#xff0c;上传完毕之后给出反馈 代码实战 1、客户端代码 package com.heima;import java.io.*; import java.net.Socket;public class Client {publi…

如何使用python编译器来编写代码,不使用anaconda和pycharm

winR 输入cmd 直接输入pip list即可查看已经安装的包有哪些 惊奇地发现我已经安装过这些包 absl-py 1.3.0 astunparse 1.6.3 cachetools 4.2.4 certifi 2022.9.24 charset-normalizer 2.1.1 gast 0.3.3 google-auth 1.35.0 google-auth-oauthlib 0.4.6 google-pasta 0.2.0 grp…

ubuntu22.04使用共享文件设置

从ubuntu20.04开始&#xff0c;设置共享文件就很麻烦 第一步&#xff1a; 安装samba&#xff1a; sudo apt install samba第二步; 创建一个共享文件夹 我以桌面Desktop为例子 第三步&#xff1a; 设置密码&#xff1a; sudo smbpasswd -a ygc第四步&#xff1a; sudo vim …

第五章——文件内容显示

5.1 浏览普通文件内容 注意 以上命令的语法&#xff1a;命令 【option】所查文件名 set命令 显示5到10行的信息 [rootcomeon ~]#sed -n 5,10p /etc/passwd 5.2 过滤文件内容显示--grep 语法 grep 【option】... 关键字符串 所查文件名... 使用特殊符号进行字符串的匹配 5…

【AI视野·今日Robot 机器人论文速览 第四十四期】Fri, 29 Sep 2023

AI视野今日CS.Robotics 机器人学论文速览 Fri, 29 Sep 2023 Totally 38 papers &#x1f449;上期速览✈更多精彩请移步主页 Daily Robotics Papers HyperPPO: A scalable method for finding small policies for robotic control Authors Shashank Hegde, Zhehui Huang, Gaur…

ElasticSearch映射与模板介绍

一、前言 前面有相关系列文章介绍了ES的基本概念和各种版本SDK的使用&#xff0c;ES现在已升级到8.5版本&#xff0c;有些概念和SDK用法都有很大变化&#xff0c;后续ES相关的文章会以8.3版本为基准介绍一些实际中应用需要掌握的概念以及一些比较实际的例子。 二、映射 ES环…

图层混合算法(一)

常见混合结果展示 图层混合后变暗 正常模式&#xff08;normal&#xff09; 混合色*不透明度&#xff08;100%-混合色不透明度&#xff09; void layerblend_normal(Mat &base,Mat &blend,Mat &dst,float opacity) {if (base.rows ! blend.rows ||base.cols ! b…

CEC2013:CEC2013测试函数及多种智能优化算法求解CEC2013对比

一、CEC2013测试函数 CEC2013&#xff0c;该测试集合也是目前高质量论文应用较广泛的测试集&#xff0c;CEC2013测试集函数复杂&#xff0c;非常具有挑战力。 二、多种智能优化算法求解CEC2013 2.1 本文参与求解CEC2013的智能优化算法 本文选取一些经典的智能优化算法参与测…

MySQL学习笔记22

mysqldumpbinlog实现增量备份&#xff1a; 1、什么是增量备份的核心思路&#xff1f; 1&#xff09;&#xff1a;先有全量备份&#xff1b; 2&#xff09;&#xff1a;继续有增删改数据&#xff1b; 3&#xff09;&#xff1a;再次需要备份的时候&#xff0c;不需要进行全量…

led护眼灯真的能护眼吗?过来人说说led灯是否真的能护眼

为什么需要购买台灯&#xff1f;正常的顶灯不行吗&#xff1f; 正常顶灯的光照射到桌子上时&#xff0c;已经发散的差不多了&#xff0c;无法满足看书写字环境下的需要。 看书写字时人是要低头的&#xff0c;很容易挡住顶灯照射的光&#xff0c;桌上需要光的位置正好被挡住。…

蓝桥杯每日一题2023.9.29

蓝桥杯大赛历届真题 - C&C 大学 B 组 - 蓝桥云课 (lanqiao.cn) 题目描述1 题目分析 看见有32位&#xff0c;我们以此为入手点&#xff0c; B代表字节1B 8b b代表位&#xff0c;32位即4个字节 (B) 1KB 1024B 1MB 1024KB (256 * 1024 * 1024) / 4 67108864 故答案…

map和set的具体用法 【C++】

文章目录 关联式容器键值对setset的定义方式set的使用 multisetmapmap的定义方式insertfinderase[]运算符重载map的迭代器遍历 multimap 关联式容器 关联式容器里面存储的是<key, value>结构的键值对&#xff0c;在数据检索时比序列式容器效率更高。比如&#xff1a;set…