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的通知。

相关推荐
Coding小公仔1 小时前
C++ bitset 模板类
开发语言·c++
凌肖战2 小时前
力扣网C语言编程题:在数组中查找目标值位置之二分查找法
c语言·算法·leetcode
菜鸟看点2 小时前
自定义Cereal XML输出容器节点
c++·qt
weixin_478689762 小时前
十大排序算法汇总
java·算法·排序算法
luofeiju3 小时前
使用LU分解求解线性方程组
线性代数·算法
SKYDROID云卓小助手3 小时前
无人设备遥控器之自动调整编码技术篇
人工智能·嵌入式硬件·算法·自动化·信号处理
悲伤小伞3 小时前
linux_git的使用
linux·c语言·c++·git
ysa0510303 小时前
数论基础知识和模板
数据结构·c++·笔记·算法
GEEK零零七3 小时前
Leetcode 1103. 分糖果 II
数学·算法·leetcode·等差数列
今天背单词了吗9804 小时前
算法学习笔记:7.Dijkstra 算法——从原理到实战,涵盖 LeetCode 与考研 408 例题
java·开发语言·数据结构·笔记·算法