0
点赞
收藏
分享

微信扫一扫

C++之线程库(八千字长文详解)

C++之线程库

前言

C++的线程库,不是只有线程,有好几个库,包括了创建线程,锁,条件变量,原子操作等待

thread类

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

构造函数

image-20230823152815411

线程函数的参数

关于线程函数的参数是以值拷贝的方式拷贝到线程栈空间中的,因此:即使线程参数为引用类型,在线程中修改后也不能修改外部实参,因为其实际引用的是线程栈中的拷贝,而不是外部实参。

mutex类

在多线程的情况下,有可能会出现数据不一致的问题!

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

int g_val = 0;
void func1(int n)
{
	for (size_t i = 0; i < n; i++)
	{
		g_val++;
	}
}


int main()
{
	thread t1(func1,100000);
	thread t2(func1,100000);

	t1.join();
	t2.join();
	cout << g_val << endl;
}

image-20230825162805484

==为了解决这个问题!所以我们可以通过加锁来解决!C++也给我们提供了mutex库用==

mutex类的构造函数

atomic类

上面的数据不一致问题除了使用加锁的,还能使用原子操作!

**数据不一致的本质原因是因为++这个操作本身不是原子的!——那么我们只要让这个操作本身就变成原子的不就好了? **

==原子操作一般都是操作系统提供的!——CAB(Compare and Swap)==

==回到最开始我们如果不加锁的保持数据一致性呢?==

#include<thread>
#include<iostream>
#include<vector>
#include<atomic>
using namespace std;
atomic<int> g_val = 0;//原子操作!
void func1(int n)
{
	for (size_t i = 0; i < n; i++)
	{
		g_val++;
	}
}
int main()
{
	thread t1(func1,1000);
	thread t2(func1,1000);

	t1.join();
	t2.join();
	cout << g_val << endl;
}

image-20230825210336562

==这样就成功了!——这个比里面加锁快!比里面加锁慢!——因为CAS会导致有些++操作会被放弃!==

但是上面有一个问题,那就是在多线程中,我们不推荐使用全局的对象!

所以我们可以用一个更好的方案

#include<thread>
#include<iostream>
#include<vector>
#include<atomic>

int main()
{
	atomic<int> aval = 0;
	auto func = [&aval](int n)
	{
		for (size_t i = 0; i <n; i++)
		{
			++aval;
		}
	};
	thread t1(func,3000);
	thread t2(func,1000);

	t1.join();
	t2.join();
	cout << aval << endl;
}

==这就是lambda表达式的好处!可以捕抓局部的变量!==

这样也可以看出,在栈上的变量不一定是线程安全的!因为有lambda表达式可以用来捕抓栈上的变量!——==只要能被多个线程访问到都要考虑是不是线程安全的!==

lock_guard类

int main()
{
	atomic<int> aval = 0;
	mutex mtx;

	auto func = [&](int n)
	{
		for (size_t i = 0; i <n; i++)
		{
			aval++;
			mtx.lock();
			int* a = new int();//如果这个new抛异常了该怎么办?
			cout << this_thread::get_id() << ":" << aval << endl;
			mtx.unlock();
		}
	};
	thread t1(func,3000);
	thread t2(func,1000);

	t1.join();
	t2.join();
	cout << aval << endl;
}

这样的情况会不会造成一个线程抛异常后退出,但是锁却没有释放?

==我们可以使用RAII的思想去解决这个问题!构造的时候加锁!析构的时候解锁!让其出了作用域后自动的调用析构函数解锁即可!==

==库里面就已经帮我们封装好了!lock_gurad类==

image-20230825221051414

我们可以看到他主要就是给我们提供了一个构造和析构!

这个锁是一个模板,只要是锁类型的都可以!

int main()
{
	atomic<int> aval = 0;
	mutex mtx;

	auto func = [&](int n)
	{
		for (size_t i = 0; i <n; i++)
		{
            {
                lock_guard<mutex> lock(mtx);//这样就可以保证出了作用域锁自动的释放!
                int* a = new int();
                cout << this_thread::get_id() << ":" << aval << endl;
                //我们还可以通过作用域来控制锁的范围!
            }
            aval++;
		}
	};
	thread t1(func,3000);
	thread t2(func,1000);

	t1.join();
	t2.join();
	cout << aval << endl;
}

unique_lock

unique_lock是比lock_gurad==更加灵活==的一个锁管理对象!

==unique_lock具有lock_guard的所有特性!——例如构造的时候获取锁!析构的时候释放锁!同时还提供了更多的可可选操作!==

构造函数

image-20230826110735174

condition_variable类

C++给我们提供的关于条件变量的类!

构造函数

image-20230826092728064

唤醒操作函数

image-20230826093511337

notify_one唤醒该条件变量下的一个线程!(在linux下面相当于pthread_cond_signal函数)

notify_all唤醒该条件变量下面的所有线程!(相当linux下面的pthread_cond_broadcast)

条件变量的应用——支持两个线程交替打印!一个打印奇数,一个打印偶数!

错误写法!

#include<thread>
#include<iostream>
#include<vector>
int main()
{
	int val = 0;//这是共享资源!
	thread Odd([&] {
		while (val <100)
		{
			if (val % 2 == 1)
			{
				cout << this_thread::get_id() << ":" << val << endl;
				++val;
			}
		}

		});
	thread Even([&] {
		while (val<100)
		{
			if (val % 2 == 0)
			{
				cout << this_thread::get_id() << ":" << val++ << endl;
				++val;
            }
		}
		});

	Odd.join();
	Even.join();
	return 0;
}
举报

相关推荐

0 条评论