C++多线程:原子操作和自旋锁

文章目录

原子操作

什么是原子操作

原子操作简单来说就是对于一个数据不可再细分的操作,例如读,写等基本操作就是原子操作。

为什么要有原子操作

当我们写出以下代码并运行的时候,我们会惊讶于结果的不可思议,结果会在10000-20000之间来回横跳,为什么?因为线程并发的时候,两个线程对于同一变量的修改可能会在时间点上重合并覆盖,导致一些步骤丢失。为了解决这个问题可以使用我们以前学到的互斥锁进行限制,然而,为了一个基本数据的每次修改都产生一个锁,对资源的开销是很大的,我们迫切需要一个更优雅的解决办法,让一个数据类型自带这种功能:在进行原子操作时自发产生互斥性。我们不可能为每一个数据类型都创造对应新的数据类型,但可以进行模板编程。于是, atomic 模板类应运而生。

cpp 复制代码
int value = 0;
void testFunc() {
	for (size_t i = 0; i < 10000; i++){
		value++;
	}
}

int main(){
	thread t1(testFunc);
	thread t2(testFunc);
	t1.join(); t2.join();
	cout<<value<<endl;
}

基本实现

atomic模板类该如果和使用?这里给出代码看注释讲解。

需要注意的是,在模板类中拷贝和赋值操作被删除。

cpp 复制代码
#include<iostream>
#include<thread>
#include<atomic>		//原子操作头文件
//atomic 模板类:atomic<int> foo = 1;
using namespace std;
int value = 0;
atomic<int> a_value = 0;
void testFunc() {
	for (size_t i = 0; i < 10000; i++){
		value++;
		a_value++;
	}
}

void test1() {
	thread t1(testFunc);
	thread t2(testFunc);
	t1.join();
	t2.join();
	cout << value << endl;
	cout << a_value << endl;
}

int main(){
	test1();
	return 0;
}

我们发现,通过原子操作,数据精确无误地进行了修改。

store、load、exchange 操作

因为拷贝、赋值操作在atomic模板中被删除,所以模板也提供了更安全的函数实现方式来实现数据的存储(写)、加载(读)、交换(改)的操作,分别是store、load、exchange成员函数。对于内存操作顺序,也给出了枚举类型,列举如下,而实际操作中如果不提严苛的要求,往往省略。

cpp 复制代码
/*
memory_order:内存顺序
强顺序:执行顺序和代码顺序一致
弱顺序:执行顺序会被处理器适当调整
	  relaxed:对于执行顺序不做要求
无效:consume:本线程中,后续原子操作必须在本原子类型操作后再开始
无效:acquire:本线程中,后续原子读操作必须在本原子类型操作后再开始
	  release:本线程中,后续原子写操作必须在本原子类型操作后再开始
无效:acq_rel:acquire和release的结合
	  seq_cst:全部读写都按照顺序
*/

关于三个成员函数的操作给出示例代码:

cpp 复制代码
#include<iostream>
#include<atomic>
atomic_int iNum = 2;	//相当于atomic<int> iNum = 2;
//可以进入atomic头文件中找到更多形如atomic_xxx的类型,便于书写
void test2(){
	//写操作
	iNum.store(4);
	cout << iNum << endl;
	//读操作
	int a = iNum.load();
	cout << a << endl;
	//修改操作
	iNum.exchange(2);
	cout << iNum << endl;
}
int main() {
	test2();
	return 0;
}

自旋锁的简单实现

在atomic模板类中,有一个类型与众不同:atomic_flag,常常用它来做自旋锁。

自旋锁有什么性质

atomic_flag:无锁的原子类型

<1>.test_and_set(): 没有设置标记,设置一下,如果设置了,返回true

<2>.clear(): 清除标记,让下一次<1>返回false

其他原子类型可以使用<3>.is_lock_free()来判断是否无锁

互斥锁:资源竞争的时候申请者进入挂起状态,需要被唤起:notify系列
自旋锁:不会挂起,而是一直循环等待锁的释放,无需唤起:无需notify

自旋锁实例代码

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

using namespace std;
atomic_flag Lock = ATOMIC_FLAG_INIT;	//其实就是{}
void test3(int n) {
	while (Lock.test_and_set()){
		printf("等待中......%d\n", n);
	}
	printf("线程完成:%d\n", n);
}

void test4(int n) {
	printf("线程开始启动:%d\n", n);
	this_thread::sleep_for(1ms);
	Lock.clear();
	printf("线程运行结束\n");
}
int main() {
	Lock.test_and_set();
	thread t1(test3, 1);
	thread t2(test4, 2);
	t1.join();
	t2.join();
	return 0;
}

上述代码中,线程1一直处于while循环中在此期间可以做循环内的事情,在线程2执行完后立即跳出循环,继续往下执行,并没有需要来自线程2的通知。

相关推荐
Lenyiin24 分钟前
《 C++ 修炼全景指南:十 》自平衡的艺术:深入了解 AVL 树的核心原理与实现
数据结构·c++·stl
程序猿进阶44 分钟前
如何在 Visual Studio Code 中反编译具有正确行号的 Java 类?
java·ide·vscode·算法·面试·职场和发展·架构
Eloudy1 小时前
一个编写最快,运行很慢的 cuda gemm kernel, 占位 kernel
算法
程序猿练习生1 小时前
C++速通LeetCode中等第5题-无重复字符的最长字串
开发语言·c++·leetcode
king_machine design1 小时前
matlab中如何进行强制类型转换
数据结构·算法·matlab
西北大程序猿1 小时前
C++ (进阶) ─── 多态
算法
无名之逆1 小时前
云原生(Cloud Native)
开发语言·c++·算法·云原生·面试·职场和发展·大学期末
头发尚存的猿小二1 小时前
树——数据结构
数据结构·算法
好蛊1 小时前
第 2 课 春晓——cout 语句
c++·算法
山顶夕景2 小时前
【Leetcode152】分割回文串(回溯 | 递归)
算法·深度优先·回溯