【C++11】 线程库的使用

news2024/9/22 14:40:51

文章目录

  • 1 线程库的基本使用
    • 1.1 thread
    • 1.2 this_thread
    • 1.3 线程函数参数
  • 2 mutex
    • 2.1 mutex的基本使用
    • 2.2 mutex系列锁
    • 2.3 lock_guard与unique_lock
  • 3 原子操作
  • 4 条件变量


1 线程库的基本使用

1.1 thread

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

👉【C++11线程库地址】👈
在这里插入图片描述
上面这些接口是不是非常熟悉,都是我们在Linux中经常使用的套路,但是我们可以看看他的构造函数:
在这里插入图片描述我们可以看见构造函数用了可变参数模板,这样我们在传递参数的时候再也不用像Linux中那样用将数据打包成一个类对象来传递,我们可以直接通过可变参数模板直接传入即可。
比如这样:

void fun(int m)
{
	for (int i = 0; i < m; ++i)
		cout << i << endl;
}

int main()
{
	thread t1(fun, 10);
	thread t2(fun, 20);

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

	return 0;
}

打印结果:
在这里插入图片描述至于为啥打印的毫无规律是由于多个线程同时抢占显示器这种临界资源而造成的,处理方式可以加锁,我们在后面会详细讲解。

同时我们注意到在thread的构造函数中删除了拷贝构造,这其实也很好理解,因为并没有实际意义。还提供了thread的移动赋值,这个在有些场景下会很有用。

其实上面我们的方式我们也是用了函数指针的方式来实现,但是一般我们不喜欢这样用,而更加青睐于lambda,比如下面这种用法:

int main()
{
	int m, n;
	cin >> m >> n;
	
	thread t1([m, n] {
		for (int i = 0; i < m; ++i)
			cout << "" << i << endl;
		});

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

这种方式写起来看起就比较有意思了,但是假如我们想要创建100个线程来实现上面的功能,一个一个写肯定是很麻烦的,我们可以采用下面这种方式:

int main()
{
	int m;
	cin >> m;
	vector<thread> ths(m);
	
	for (int i = 0; i < m; ++i)
	{
		ths[i] = thread([] {
			for (int i = 0; i < 5; ++i)
				cout << this_thread::get_id()<<":" << i << endl;
			});
		
	}

	for (auto& e : ths)
		e.join();
	return 0;
}

这种方式我们就将匿名对象的资源给移动赋值了。代码中我们还给了this_thread::get_id(),这个又是什么鬼呢?

1.2 this_thread

这个this_thread::是一个命名空间,空间中有着下面的类:
在这里插入图片描述上面代码中由于对象还没有被创建出来,所以就用了全局域中的this_thread命名空间中的get_id,这个用法其实类似于Linux中的用法。另外的sleep_until和sleep_for顾名思义,sleep_until是休眠到一个具体的时间点(比如2023 6 21),而sleep_for是休眠一个具体时间段(比如200毫秒). 具体的大家可以自行下去看看样例,我这里就不再多赘述了👉【this_thread】👈

这里面还有一个比较有意思的:yield 作用是让出当前线程的时间片.
在讲解yield之前我们再来补充一个小知识点:无锁编程
就拿我们刚开始讲的栗子来说:两个线程并发竞争显示器这种临界资源的时候打印出的无规律数据,一般的解决方式我们可以采用加锁的方式来处理,但是我们除了这种方式外还可以采用原子操作来解决。
而要实现原子操作就必不可免的要利用CAS原子操作,那什么是CAS呢?
CAS是Compare And Swap的缩写(也可以是Compare And Set),比较与交换,通常指的是这样一种原子操作:针对一个变量,首先比较它的内存值和某个期望值是否相同,如果相同,就给它赋一个新值。现在几乎所有的CPU指令都支持CAS的原子操作,X86下对应的是 CMPXCHG 汇编指令。有了这个原子操作,我们就可以用其来实现各种无锁(lock free)的数据结构。
大家想了解更多关于CAS原子操作可以移步陈皓大佬文章👉【无锁队列】👈

1.3 线程函数参数

大家再来看看下面这段代码:

void ThreadFunc1(int& x)
{
	x += 10;
}

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

注意上面代码中我们传入的参数加了&,当我们编译的时候:
在这里插入图片描述VS2022会直接报编译错误,但是老一点的编译器是不会报错的,在线程函数中对a修改,不会影响外部实参,因为:线程函数参数虽然是引用方式,但其实际引用的是线程栈中的拷贝。那假如我们就想修改呢?我们可以用std::ref()函数,比如下面:

void ThreadFunc1(int& x)
{
	x += 10;
}

int main()
{
	int a = 10;
	thread t2(ThreadFunc1, std::ref(a));
	t2.join();
	cout << a << endl;
	return 0;
}

当我们编译运行时:
在这里插入图片描述
明显此时已经修改成功了。除此之外,我们甚至还可以用下面这种方式:

void ThreadFunc2(int* x)
{
	*x += 10;
}

int main()
{
	int a = 10;
	thread t3(ThreadFunc2, &a);
	t3.join();
	cout << a << endl;
	return 0;
}

运行结果:
在这里插入图片描述
这种通过地址的拷贝也是能够完成修改的。
注意:如果是类成员函数作为线程参数时,必须将this作为线程函数参数。


2 mutex

2.1 mutex的基本使用

C++11提供的最基本的互斥量,该类的对象之间不能拷贝,也不能进行移动。mutex最常用的三个函数:

函数名函数功能
lock()上锁:锁住互斥量
unlock()解锁:释放对互斥量的所有权
try_lock()尝试锁住互斥量,如果互斥量被其他线程占有,则当前线程也不会被阻塞

注意,线程函数调用lock()时,可能会发生以下三种情况:

  • 1️⃣如果该互斥量当前没有被锁住,则调用线程将该互斥量锁住,直到调用 unlock之前,该线程一直拥有该锁;
  • 2️⃣如果当前互斥量被其他线程锁住,则当前的调用线程被阻塞住;
  • 3️⃣如果当前互斥量被当前调用线程锁住,则会产生死锁(deadlock).

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

  • 1️⃣如果当前互斥量没有被其他线程占有,则该线程锁住互斥量,直到该线程调用 unlock 释放互斥量;
  • 2️⃣如果当前互斥量被其他线程锁住,则当前调用线程返回 false,而并不会被阻塞掉;
  • 3️⃣如果当前互斥量被当前调用线程锁住,则会产生死锁(deadlock).

我们来看看下面的代码:

long long tickets;
mutex mtu;
int main()
{
	thread t1([] {
		for (int i = 0; i < 100000; ++i)
		{
			tickets++;
		}
		});

	thread t2([] {
		for (int i = 0; i < 100000; ++i)
		{
			tickets++;
		}
		});
	t1.join();
	t2.join();
	cout << tickets << endl;
	return 0;
}

当我们运行时结果跟我们预想的一样,肯定会有并发打印的问题:
在这里插入图片描述
我们可以加锁处理:
在这里插入图片描述我们这是将锁加在里面的,能不能加在外面呢?
在这里插入图片描述当然也是可行的,那么哪种效率更高一些呢?在Linux中,我们知道一般加锁会选择粒度更小的区域中加,也就是加在里面,我们可以写一个测试用例来测试下加在里面和外面的时间差异:
先试试把锁加在for循环外面:

	size_t begin = clock();
	thread t1([] {
		mtu.lock();
		for (int i = 0; i < 100000000; ++i)
		{
			tickets++;
		}
		mtu.unlock();
		});

	thread t2([] {
		mtu.lock();
		for (int i = 0; i < 100000000; ++i)
		{
			tickets++;
		}
		mtu.unlock();
		});

	t1.join();
	t2.join();
	
	size_t end = clock();
	cout << end - begin << endl;

测试时间:
在这里插入图片描述
再试试把锁加在for循环里面:

	size_t begin = clock();
	thread t1([] {
		for (int i = 0; i < 10000000; ++i)
		{
			mtu.lock();
			tickets++;
			mtu.unlock();
		}
		});

	thread t2([] {
		for (int i = 0; i < 10000000; ++i)
		{
			mtu.lock();
			tickets++;
			mtu.unlock();
		}
		});

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

	size_t end = clock();
	cout << end - begin << endl; 

在这里插入图片描述
我们居然惊奇的发现加锁粒度更大的操作速度居然要快,为什么呢❓
其实原因主要有下面两点💯💯:

  • 1️⃣频繁加锁解锁是有消耗的
  • 2️⃣频繁的切换线程也是有代价的

当我们把锁加在for循环的里面时候,由于每进行一次++i操作都会申请锁和释放锁,并且当其中一个线程竞争到锁后另外一个线程会被切换挂起,可是由于每次++i的操作时间很短,所以线程间又可能被频繁的切换,而上下文的保存与切换是有代价的。
但是不是说粒度更大的加锁策略效率更高呢❓显然不是这样的,要具体情况具体分析。但是我们在Linux下一般建议是使用粒度更小的加锁策略。

2.2 mutex系列锁

我们来看看C++11线程库提供了哪些锁呢?
在这里插入图片描述

第一个是我们刚刚使用了的,第二个recursive_mutex是什么呢❓
这个是递归互斥锁 ,当我们递归调用自己的时候而造成死锁的解决方式。这是由于我们申请了锁后没有释放而继续递归自己下去再重复申请改锁而造成了🔥死锁🔥。
这时我们就可以使用recursive_mutex,他会在申请锁前先释放该锁,出了该函数栈帧后又会重新申请该锁。recursive_mutex允许同一个线程对互斥量多次上锁(即递归上锁),来获得对互斥量对象的多层所有权,释放互斥量时需要调用与该锁层次深度相同次数的 unlock(),除此之外,
std::recursive_mutex 的特性和 std::mutex 大致相同。
另外两个:timed_mutex和recursive_timed_mutex是与时间有关的,比较简单。我这里大概提一下 std::timed_mutex的基本用法:
try_lock_for()

  • 接受一个时间范围,表示在这一段时间范围之内线程如果没有获得锁则被阻塞住(与std::mutex 的 try_lock()不同,try_lock如果被调用时没有获得锁则直接返回false),如果在此期间其他线程释放了锁,则该线程可以获得对互斥量的锁,如果超时(即在指定时间内还是没有获得锁),则返回false。

try_lock_until()

  • 接受一个时间点作为参数,在指定时间点未到来之前线程如果没有获得锁则被阻塞住,如果在此期间其他线程释放了锁,则该线程可以获得对互斥量的锁,如果超时(即在指 定时间内还是没有获锁),则返回 false。

其他的大家可以自己到官网去看:👉【Mutex】👈

🌀🌀🌀虽然上面加锁可以解决多线程中的问题,但是一定要控制得当,否则很容易造成死锁,那还有没有不用加锁就可以解决的方法呢❓
也就是下面我们要介绍的原子操作

2.3 lock_guard与unique_lock

通过上面我们的讲解我们知道:锁控制不好时,可能会造成死锁,最常见的比如在锁中间代码返回,或者在锁的范围内抛异常。因此:C++11采用RAII的方式对锁进行了封装,即lock_guardunique_lock

std::lock_gurad 是 C++11 中定义的模板类。定义如下:

template<class _Mutex>
class lock_guard
{
public:
	// 在构造lock_gard时,_Mtx还没有被上锁
	explicit lock_guard(_Mutex& _Mtx)
		: _MyMutex(_Mtx)
	{
		_MyMutex.lock();
	}
	// 在构造lock_gard时,_Mtx已经被上锁,此处不需要再上锁
	lock_guard(_Mutex& _Mtx, adopt_lock_t)
		: _MyMutex(_Mtx)
	{}
	~lock_guard() _NOEXCEPT
	{
		_MyMutex.unlock();
	}
	lock_guard(const lock_guard&) = delete;
	lock_guard& operator=(const lock_guard&) = delete;
private:
	_Mutex& _MyMutex;
};

这种RAII的设计模式我们在智能指针阶段还会详细讲解,大家在这里可以先大概了解下原理。就是利用对象创建和对象销毁时调用构造函数和析构函数来帮助加锁和解锁操作

通过上述代码可以看到,lock_guard类模板主要是通过RAII的方式,对其管理的互斥量进行了封装,在需要加锁的地方,只需要用上述介绍的任意互斥体实例化一个lock_guard,调用构造函数成功上锁,出作用域前,lock_guard对象要被销毁,调用析构函数自动解锁,可以有效避免死锁问题。

lock_guard的缺陷太单一,用户没有办法对该锁进行控制,因此C++11又提供了
unique_lock

与lock_gard类似,unique_lock类模板也是采用RAII的方式对锁进行了封装,并且也是以独占所有权的方式管理mutex对象的上锁和解锁操作,即其对象之间不能发生拷贝。在构造(或移动(move)赋值)时,unique_lock 对象需要传递一个 Mutex 对象作为它的参数,新创建的unique_lock 对象负责传入的 Mutex 对象的上锁和解锁操作。使用以上类型互斥量实例化unique_lock的对象时,自动调用构造函数上锁,unique_lock对象销毁时自动调用析构函数解锁,可以很方便的防止死锁问题。

与lock_guard不同的是,unique_lock更加的灵活,提供了更多的成员函数:

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

👉【lock_guard】👈
👉【unique_lock】👈


3 原子操作

C++11中引入了原子操作。所谓原子操作:即不可被中断的一个或一系列操作,C++11引入的原子操作类型,使得线程间数据的同步变得非常高效。
在这里插入图片描述👉【atomic】👈

比如之前我们抢票的代码还可以这样写:

atomic<long long> tickets;
mutex mtu;
int main()
{
	thread t1([] {
		for (int i = 0; i < 100000; ++i)
		{
			tickets++;
		}
		});

	thread t2([] {
		for (int i = 0; i < 100000; ++i)
		{
			tickets++;
		}
		});
	t1.join();
	t2.join();
	cout << tickets << endl;
	return 0;
}

运行结果:
在这里插入图片描述

我们使用原子操作不用加锁也可以正确的完成。其实原子操作的底层是用的CAS原子操作来完成的。

在C++11中,程序员不需要对原子类型变量进行加锁解锁操作,线程能够对原子类型变量互斥的
访问。更为普遍的,程序员可以使用atomic类模板,定义出需要的任意原子类型。

atmoic<T> t;    // 声明一个类型为T的原子类型变量t

注意:原子类型通常属于"资源型"数据,多个线程只能访问单个原子类型的拷贝,因此在C++11中,原子类型只能从其模板参数中进行构造,不允许原子类型进行拷贝构造、移动构造以及operator=等,为了防止意外,标准库已经将atmoic模板类中的拷贝构造、移动构造、赋值运算符重载默认删除掉了。


4 条件变量

先来看看condition_variable类中有哪些成员函数?
在这里插入图片描述这个用法与Linux中基本一致,我在这里就不再多讲了。
现在假如我们要实现下面的场景,我们究竟应该怎么做?
支持两个线程交替打印,一个打印奇数,一个打印偶数

这样实现有两个注意点:

  • 1️⃣要保证一个线程先运行,一个后运行;
  • 2️⃣要防止一个线程不断运行。

所以我们就可以写出下面的代码:

int num = 1;
mutex mtu;
condition_variable cdv;
int main()
{
	thread t1([=] {
		
		for (;num<100;num++)
		{
			unique_lock<mutex> lock(mtu);
			if (num % 2 == 0)
				cdv.wait(lock);
			cout << this_thread::get_id() << ":" << num << endl;
			cdv.notify_one();
		}
		
		});

	thread t2([] {
		
		for (; num<100; num ++)
		{
			unique_lock<mutex> lock(mtu);
			if(num%2!=0)
				cdv.wait(lock);
			cout << this_thread::get_id() << ":" << num << endl;
			cdv.notify_one();
		}
		
		});

	t1.join();
	t2.join();
	return 0;
}

但是上面的代码可能会存在着问题,因为在条件判断的时候我们没有加锁,这就导致有可能其中一个线程打印出了101这样的数据,所以我们可以写一个死循环,在循环体里面判断即可。

thread t1([=] {
		for (; ;num++)
		{
			unique_lock<mutex> lock(mtu);
			if (num >= 100)	break;
			if (num % 2 == 0)
				cdv.wait(lock);
			cout << this_thread::get_id() << ":" << num << endl;
			cdv.notify_one();
		}
		
		});

	thread t2([] {
		
		for (; ; num++)
		{
			unique_lock<mutex> lock(mtu);
			if (num > 100)	break;
			if(num%2!=0)
				cdv.wait(lock);
			cout << this_thread::get_id() << ":" << num << endl;
			cdv.notify_one();
		}
		
		});

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

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

相关文章

研究一下「pnpm」这个神奇的包管理工具

最近搬砖 &#x1f9f1; 在搞前端项目部署优化 &#x1f3a1;&#xff0c;大部分项目的包管理工具都已经从 npm/yarn 替换成了 pnpm&#xff0c;整体来看无论是在 install 或是 build 阶段都提速了不少 &#x1f680;&#xff0c;借此时机&#xff0c;做个总结&#xff01;&…

TypeScript 中【类型断言】得使用方法

类型断言的概念 有些时候开发者比TS本身更清楚当前的类型是什么&#xff0c;可以使用断言&#xff08;as&#xff09;让类型更加精确和具体。 类型断言&#xff08;Type Assertion&#xff09;表示可以用来手动指定一个值的类型。 类型断言语法&#xff1a; 值 as 类型 或 <…

vue3 实现多层级列表

文章目录 需求背景解决效果index.vue视频效果 需求背景 需要在统一个列表下&#xff0c;实现商品和规格得管理和联动 解决效果 index.vue <!--/*** author: liuk* date: 2023/7/7* describe: 商品列表 */--> <template><div class"container">&…

textarea自适应高度二——(设置隐藏div获取高度和仿element-ui组件)

文章目录 前言一、通过隐藏div的方式来设置文本域自适应高度1. 新增一个文本域样式一个的dom&#xff0c;但是里面的textarea改为div2. 隐藏div的class3.设置文本域高度的方法 二、仿element-ui组件设置textarea自适应高度1.element-ui中自适应效果2. 看源码&#xff0c;盘逻辑…

病毒专题丨 plugx病毒

一、病毒简述 之前分析了一下&#xff0c;分析的较为简单&#xff0c;这次又详细分析了一下。 文件名称 00fbfaf36114d3ff9e2c43885341f1c02fade82b49d1cf451bc756d992c84b06 文件格式 RAR 文件类型(Magic) RAR archive data, v5 文件大小 157.74KB SHA256 00fbfaf36114d3ff9e…

【编程中的数学】:冰雹猜想

今天和大家分享一个令人着迷的数学谜题——冰雹猜想。这个谜题曾在1976年引起轰动&#xff0c;当时《华盛顿邮报》以头版头条刊登了一篇关于它的报道。让我们一起探索这个数学游戏的奥秘。 70年代中期&#xff0c;美国一所名牌大学的校园内兴起了一种数学游戏&#xff0c;这个游…

微信小程序使用vant时间选择器二次封装成自定义区间时间选择

目录 1.引入vant组件库 2.wxml页面 3.js页面 1.引入vant组件库 1.安装vant # 通过 npm 安装 npm i vant/weapp -S --production # 通过 yarn 安装 yarn add vant/weapp --production # 安装 0.x 版本 npm i vant-weapp -S --production 2.将 app.json 中的 "style&quo…

2-需求

目录 1.需求的定义 1.1.用户需求 1.2.软件需求 PS&#xff1a;软件需求规格说明书 2.为什么有需求&#xff1f; PS&#xff1a;为什么需求对软件测试人员如此重要&#xff1f; 3.测试人员眼里的需求 4.如何深入了解需求&#xff1f; 4.1.参加需求评审会议 4.2.查阅文…

数据结构初阶--二叉树OJ1

目录 二叉树的最大深度思路分析代码实现 相同的树思路分析代码实现 单值二叉树思路分析代码实现 二叉树的前序遍历思路分析代码实现 翻转二叉树思路分析代码实现 对称二叉树思路分析代码实现 另一棵树的子树思路分析代码实现 二叉树的最大深度 先来看题目描述 思路分析 题目…

QT学习—串口LED小项目

前言——博主刚开始接触QT&#xff0c;本文参考博主嵌入式大杂烩的一篇博文易懂 | 手把手教你编写你的第一个上位机&#xff0c;初步学习一下QT开发。 文章目录 一、QT安装二、新建工程三、创建上位机界面3.1 修改控件名3.2 添加槽函数 四、上位机程序打包五、上位机测试六、总…

不要用 in + 子查询

前两天我的 VIP 用户向我抛出了一个 SQL 问题&#xff0c;他的 MySQL 是 8.x版本&#xff1a; 大概意思如下 sql &#xff1a; select * from A where id in (select max(id) as id from A where task_id in(1,2,3) group by task_id );这个 A 表中是有 task_id 这个索引的。 …

【转换】编码转换工具笔记

应用场景 应用场景是程序整合第三方库多平台运行&#xff0c;第三方库window平台编译&#xff0c;代码移植到linux出现bom问题 思考解决 windows使用utf-8编码&#xff0c;linux使用utf-8无bom编码 工具主要针对utf-8编码文件&#xff0c;能够批量添加删除BOM&#xff0c;无…

SpringBoot获取项目日志

目的 对于布署在远端的服务&#xff0c;我们想快速的获取到日志。对于使用了日志服务&#xff0c;也可能因为上报间隔太长&#xff0c;日志不够实时。 所以想通过一些方式&#xff0c;可以不用进入到容器内也可以简单快速获取到日志&#xff0c;而且是实时的日志。目标就是获…

c语言进阶-动态内存管理

重点学习内容 动态内存管理四大函数 Malloc 内存申请函数 返回值是无类型的指针&#xff0c;指向分配的内存的首地址。申请失败会返回空指针。 malloc返回值是void*类型&#xff0c;使用时需要强制转换成所需类型。 malloc和free匹配使用&#xff0c;但是如果不free释放内存&…

解析3D视觉系统在引导金属件上下料中的应用

原创 | 文 BFT机器人 引言 Introduction 机器视觉技术作为一种高科技的智能化技术&#xff0c;正在工业生产领域发挥着越来越重要的作用。它利用计算机视觉技术&#xff0c;通过获取、处理和分析图像&#xff0c;实现对产品和工艺过程的监测、检测和控制。 随着人工智能技术的…

[SUCTF2019]hardcpp

前言 又遇到ollvm了 解混淆 可以直接用angr运行脚本去除除控制流平坦化&#xff0c;最好在ancoda等管理环境里面安装angr不然问题很多 https://github.com/Pure-T/deflat 去除前 去除后&#xff0c;它将多余的直接nop了 分析 主要加密区域位于匿名函数这一块&#xff0c…

前端学习——Web API(Day1)

Web API基本认知 Web API 基本认知 作用和分类 DOM DOM树 DOM对象 获取DOM对象 根据CSS选择器来获取DOM元素 <!DOCTYPE html> <html lang"zh-CN"> <head><meta charset"UTF-8"><meta http-equiv"X-UA-Compatible" …

【教学类-36-08】转学“纪念册“留念(生肖用midjounery-niji)

作品样式 背景需求&#xff1a; 即将离开班级&#xff0c;我想收集一份28个孩子的绘画册做——留念转学纪念册. 材料准备&#xff1a; 幼儿照片 ——3月初到中6班拍摄的幼儿手持学号名字纸的照片&#xff08;为了背诵幼儿信息而拍摄的照片&#xff0c;统一竖版&#xff09; 生…

jmeter 连接数据库常见报错

1. 不允许主机连接到MySQL 报错信息&#xff1a; Response message:java.sql.SQLException: Cannot create PoolableConnectionFactory (null, message from server: "Host 192.168.1.6 is not allowed to connect to this MySQL server") 说明&#xff1a;本机的地…

代码随想录day10

232. 用栈实现队列 思路&#xff1a;用两个list去模拟栈的操作&#xff0c;一个入栈list&#xff0c;一个出栈list. 并且了解栈的操作&#xff0c;pop,peek,push. 代码&#xff1a; def __init__(self):self.stack1 [] #入栈self.stack2[] #出栈def push(self, x: int) ->…