C++11支持并发库

注:本章节内容基于Linux学习多线程之后,默认已经有了进程线程的基础,所以重点讲解库的使用,不会讲解进程线程相关的概念以及基础知识。

1、thread库

thread库底层是对各个系统的线程库的封装,比如pthread(Linux)和thread(Windows),所以C++11的thread库的第一个特点是可以跨平台 ,第二个特点就是面向对象,并且融合了一些C++11的语言特点(比如右值引用、可变模版参数等),用起来会更加方便。

线程创建有以下四种构造函数:

最常用的是第二个,它支持传一个可调用对象和参数即可。相比于pthread_create而言,这里不再局限于只传函数指针,并且参数传递也会更加方便。

cpp 复制代码
#include <iostream>
#include <thread>

using namespace std;

void Print(int n, int i)
{
	for (; i < n; i++)
	{
		cout << this_thread::get_id() << ":" << i << endl;
	}
	cout << endl;
}

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

	cout << t1.get_id() << endl;
	cout << t2.get_id() << endl;

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

	return 0;
}

join是主线程结束前,还需要阻塞等待从线程,否则主线程结束了,也就意味着进程结束,从线程可能还在运行时就被终止了。

class thread::id是一个thread的内部类表示线程id。因为各个平台的线程id表示类型不同,所以只能用一个类进行封装。线程对象可以用get_id获取线程id,在执行体内可以通过this_thread::get_id()来获取。

2、this_thread

this_thread是一个命名空间,主要封装了线程相关的4个全局接口函数:

get_id:当前执行线程的线程id;

yield:主动让出当前线程的执行权,让其他线程先执行;

sleep_for:阻塞当前线程执行,至少经过指定的sleep_duration;

sleep_until:阻塞当前线程执行,直至抵达指定的sleep_time。

cpp 复制代码
#include <iostream>
#include <thread>
#include <chrono>

using namespace std;

int main()
{
	for (int i = 10; i >= 0; i--)
	{
		std::cout << i << std::endl;
		std::this_thread::sleep_for(std::chrono::seconds(1));
	}

	return 0;
}

3、mutex

mutex是封装互斥锁的类,用于保护临界区的共享资源。

cpp 复制代码
#include <iostream>
#include <thread>
#include <mutex>

using namespace std;

void Print(int n, int& rx, mutex& rmtx)
{
	rmtx.lock();
	for (int i = 0; i < n; i++)
	{
		rx++;
	}
	rmtx.unlock();
}

int main()
{
	int x = 0;
	mutex mtx;

	thread t1(Print, 1000, ref(x), ref(mtx));
	thread t2(Print, 2000, ref(x), ref(mtx));

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

	cout << x << endl;

	return 0;
}

这里要注意,传参时必须要用ref()传参,线程中拿到的才是x和mtx的引用!

thread 内部机制:构造 thread 对象时,所有传入的实参都会被拷贝到线程内部的存储空间;启动线程时,再把内部拷贝出来的值转发给目标函数 Print;默认转发是值语义,就算你写&x,它也会复制一份 x 的副本,并把副本传给 Print。

而std::ref(x) 会生成一个 reference_wrapper<T> 包装对象。包装器本身可以被自由拷贝;内部存储原始变量的地址;thread 在转发参数时,如果遇到 reference_wrapper,会自动解包,转为左值引用传给函数。

所以像 mutex 这种禁止拷贝的类型,不传 ref 必然编译失败

所以为了尽量避免上面这种"坑",我们还可以用下面这种写法(lambda表达式):

cpp 复制代码
#include <iostream>
#include <thread>
#include <mutex>

using namespace std;

int main()
{
	int x = 0;
	mutex mtx;

	auto Print = [&x, &mtx](int n) {	
		mtx.lock();
		for (int i = 0; i < n; i++)
		{
			x++;
		}
		mtx.unlock();
	};

	thread t1(Print, 1000);
	thread t2(Print, 2000);

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

	cout << x << endl;

	return 0;
}

思考一下:如果我把锁放在循环里面,程序运行结果仍然正确吗?性能会怎样?

cpp 复制代码
void Print(int n, int& rx, mutex& rmtx)
{
	for (int i = 0; i < n; i++)
	{
		rmtx.lock();
		rx++;
		rmtx.unlock();
	}
}

先说结论:两种写法结果都正确,但是锁的粒度不同,开销差距明显

原始代码是粗粒度锁,线程一旦拿到锁,一次性执行全部 n 次自增,全程持有锁。循环内部上锁就变成了细粒度锁,每执行一次 rx++,就要 lock 并 unlock 一次。这就会涉及操作系统内核调用(用户态 ↔ 内核态切换),内部包含原子指令、缓存同步、线程阻塞唤醒机制,频繁争抢锁会造成 CPU 缓存颠簸。大量重复的加解锁带来巨大额外开销,CPU 花费大量时间处理同步,而不是业务计算rx++。所以思考版本代码运行更慢!

4、lock_guard

lock_guard是C++11提供的支持RAII方式管理互斥锁资源的类,这样可以有效防止因为异常等原因导致的死锁问题。

cpp 复制代码
#include <iostream>
#include <thread>
#include <mutex>

using namespace std;

int main()
{
	int x = 0;
	mutex mtx;

	auto Print = [&x, &mtx](int n) {	
		lock_guard<mutex> lock(mtx);
		for (int i = 0; i < n; i++)
		{
			x++;
		}
	};

	thread t1(Print, 1000);
	thread t2(Print, 2000);

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

	cout << x << endl;

	return 0;
}

5、condition_variable

condition_variable需要配合互斥锁进行使用,主要提供wait和notify系统接口。

wait需要传递一个unique_lock<mutex>类型的互斥锁,wait会阻塞当前线程,直到被notify。在进入阻塞的一瞬间,会解开互斥锁,便于其他线程获取锁并访问条件变量。

notify_one会唤醒当前条件变量上等待的其中一个线程,使用时也需要互斥锁保护,如果没有阻塞等待的线程,那么该系统接口什么事也不做。

unique_lock也是C++11提供的支持RAII方式的互斥锁资源的类,相比于lock_guard,它的功能支持更复杂。

下面演示一个经典问题:两个线程交替打印奇数和偶数

cpp 复制代码
#include <iostream>
#include <thread>
#include <mutex>
#include <condition_variable>

using namespace std;

int main()
{
	std::mutex mtx;
	condition_variable c;
	int n = 100;
	bool flag = true;

	thread t1([&]() {
		int i = 0;
		while (i < n)
		{
			unique_lock<mutex> lock(mtx);

			while (!flag)
			{
				c.wait(lock);
			}

			cout << this_thread::get_id() << ":" << i << endl;

			flag = false;
			i += 2;

			c.notify_one();
		}
	});

	thread t2([&]() {
		int j = 1;
		while (j < n)
		{
			unique_lock<mutex> lock(mtx);

			while (flag)
			{
				c.wait(lock);
			}

			cout << this_thread::get_id() << ":" << j << endl;

			flag = true;
			j += 2;

			c.notify_one();
		}
	});

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

	return 0;
}
相关推荐
Lhan.zzZ1 小时前
QML 开发中的“陷阱”:动态模型更新时,ComboBox 索引为何总是失效?
开发语言·qt
小园子的小菜1 小时前
Java 并发四大工具类深度解析:CountDownLatch、CyclicBarrier、Semaphore、Exchanger 原理、源码与面试考点
java·开发语言·面试
六月的翅膀2 小时前
OCR:RapidOCR C++/C#
c++·ocr
河西石头2 小时前
再谈C#的抽象类和接口:从“适配器”到“毛坯房”的设计哲学
开发语言·c#·接口·依赖注入·抽象类·依赖倒挂
颜x小2 小时前
[C#]——接口与继承
开发语言·c++·c#
冻柠檬飞冰走茶2 小时前
PTA基础编程题目集 7-17 爬动的蠕虫(C语言实现)
c语言·开发语言·数据结构·算法
咯哦哦哦哦2 小时前
qt creator x86交叉编译arm 配置
开发语言·qt
乐启国际旅行社有限公司2 小时前
Java线程池实战:文旅系统批量任务性能优化(团期生成/数据导出)
java·开发语言·性能优化
宸津-代码粉碎机2 小时前
告别手动Jar部署!生产级无损热部署方案,彻底解决OOM与更新失效问题
java·大数据·开发语言·人工智能·python