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;
}
相关推荐
博、、5 分钟前
智慧场馆解决方案系统开发实战:从架构设计到落地指南
开发语言·需求分析
欧特克_Glodon8 分钟前
OpenCV计算机视觉开发入门与实践<三十五>:开发视频播放器
c++·opencv·计算机视觉·音视频
二十雨辰17 分钟前
[Java]-场景题
java·开发语言
geovindu42 分钟前
java:Observer Pattern
java·开发语言·后端·观察者模式·设计模式·行为模式
Am-Chestnuts43 分钟前
通义千问任务清单怎么导出Word?用DS随心转保留层级和勾选项
开发语言·c#·word
hansang_IR1 小时前
【题解】P9753 [CSP-S 2023] 消消乐
数据结构·c++·算法
liliangcsdn1 小时前
基于信号强度的稀疏选股算法的探索分析
开发语言·python
竞赛考级题库1 小时前
202606 青少年等级考试C/C++真题一级 建议答题时长:60min
java·c语言·c++
4SAPI1 小时前
2026年大模型API接入选型指南:企业与个人用户的架构、稳定性与成本考量
大数据·开发语言·数据库·人工智能·架构·php
lmy_loveF1 小时前
go 切换go version 版本
开发语言·后端·golang