6.3 C++11 原子操作与原子类型

一、原子类型

1.多线程下的问题

在C++中,一个全局数据在多个线程中被同时使用时,如果不加任何处理,则会出现数据同步的问题。

#include <iostream>
#include <thread>
#include <chrono>
long val = 0;

void test()
{
	for (int i = 0; i < 10000000; i++) {
		val++;
	}
}

int main()
{
	auto time1 = std::chrono::duration_cast<std::chrono::milliseconds>(std::chrono::system_clock::now().time_since_epoch()).count();
	std::thread thread1(test);
	std::thread thread2(test);
	thread1.join();
	thread2.join();
	auto time2 = std::chrono::duration_cast<std::chrono::milliseconds>(std::chrono::system_clock::now().time_since_epoch()).count();

	time2 - time1;

    std::cout << "val:" << val<<" " << ((time2 - time1)/1000.0)<<"s"<<std::endl;
}

上述例子中test函数对全局变量val进行累加,并在thread1和thread2两个线程中分别处理test函数。得到的结果如下:

val:11260278 0.105s

val的值并不是期望的20000000,这是因为val++操作不是原子操作导致的。

对于val++,实际上会被拆为三步:

  • 从内存读取val值到寄存器
  • 寄存器自增
  • 将寄存器值回写到val的内存

而多线程同时操作时,实际步骤可能为:

  • thread1读取val值为0
  • thread2读取val值为0
  • thread1增加val值为1
  • thread2增加val值为1(注意与thread1增加的不是一个原始值)
  • thread1写入val,值为1
  • thread2覆盖写入val,值为1

这样就会导致两个线程的操作重复,最终结果小于20000000。

2.原子操作

所谓原子操作,指的是多线程中"最小且不可并行化"的操作,如val++语句的三步在一个线程中执行完之前,不会运行其他的进程。

在C++11之前,原子操作都是通过“互斥”(即在临界区间内,一个线程正在访问,则其他线程会等待)处理的,如互斥锁。下面则是通过添加互斥锁处理这个问题:

#include <mutex>

long val = 0;

std::mutex locker;
void test()
{
	for (int i = 0; i < 10000000; i++) {
		locker.lock();
		val++;
		locker.unlock();
	}
}

输出结果如下:

val:20000000 5.648s

可以看到val值正确,但是性能消耗非常大。

3.原子类型

C++11将原子操作抽象,引入原子类型atomic,并提供相应的操作接口(原子操作)。通过atomic实现线程间数据同步:

#include <atomic>

std::atomic_long val = 0;
void test()
{
	for (int i = 0; i < 10000000; i++) {
		val++;
	}
}

输出为:

val:20000000 2.29s

可见val值正确,且耗时比互斥锁小了很多。(因为mutex使用涉及:多次atomic原子指令操作+用户态和内核态切换、线程切换调度开销、全局锁hash queue维护等开销,所以时间更长。但是atomic只能对变量,而锁可以针对范围内的所有内容)

3.1 内置原子类型

类似于前面的atomic_long,C++11为所有内置类型都提供了对应的原子类型:

3.2 自定义类型原子类型

因为atomic为类模板,所以可以通过:

std::atomic<T> t;

创建自定义类型的原子类型,当然也可以使用此方式创建内置类型的原子类型。

atomic为作为类模板,提供了统一的操作接口:

其中is_lock_free用于判断是否有锁,load用于读取,store用于存,exchange用于交换数据。

由于原子类型属于资源类型,所以为了避免拷贝时引起的问题,atomic类模板删除了相关的拷贝构造和赋值函数。

此外,atomic到原始类型的转换也是允许的(隐式的),但非原子操作。

atomic_flag

atomic_flag是无锁的,仅支持test_and_set和clear两个接口。其中test_and_set表示:

  • 如果atomic_flag原始为false,则设置其为true,并返回false
  • 如果原始为true,则不处理,并返回true

而clear则表示将atomic_flag置为false。

所以可以使用atomic_flag实现一个自旋锁:

#include <thread>
#include <atomic>
#include <iostream>
#include <unistd.h>
using namespace std;
std::atomic_flag lock = ATOMIC_FLAG_INIT;
void f(int n) {
    while (lock.test_and_set(std::memory_order_acquire)) // 尝试获得锁
        cout << "Waiting from thread " << n << endl;      // 自旋
    cout << "Thread " << n << " starts working" << endl;
}
void g(int n) {
    cout << "Thread " << n << " is going to start." << endl;
    lock.clear();
    cout << "Thread " << n << " starts working" << endl;
}
int main() {
    lock.test_and_set();	//设为true
    thread t1(f, 1);
    thread t2(g, 2);
    t1.join();
    usleep(100);
    t2.join();
}
// 编译选项:g++ -std=c++11 6-3-3.cpp -lpthread

上述代码声明了一个全局的atomic_flag变量lock。最开始,将lock初始化为值ATOMIC_FLAG_INIT,即false的状态。

而在线程t1中(执行函数f的代码),我们不停地通过lock的成员test_and_set来设置lock为true。这里的test_and_set()是一种原子操作,用于在一个内存空间原子地写入新值并且返回旧值。因此test_and_set会返回之前的lock的值。

所以当线程t1执行join之后,由于在main函数中调用过test_and_set,因此f中的test_and_set将一直返回true,并不断打印信息,即自旋等待。

而当线程t2加入运行的时候,由于其调用了lock的成员clear,将lock的值设为false,因此此时线程t1的自旋将终止,从而开始运行后面的代码。这样一来,我们实际上就通过自旋锁达到了让t1线程等待t2线程的效果。

当然,还可以将lock封装为锁操作,比如:

void Lock(atomic_flag *lock) { while (lock.test_and_set ()); }
void Unlock(atomic_flag *lock) { lock.clear(); }

二、内存模型、顺序一致性和memory_order

原子类型为线程间数据同步提供了一定的保障,但是这是建立在顺序一致性的内存模型基础上。

1.问题

#include <thread>
#include <atomic>
#include <iostream>
using namespace std;
atomic<int> a {0};
atomic<int> b {0};
int ValueSet(int) {
    int t = 1;
    a = t;
    b = 2;
}
int Observer(int) {
    cout << "(" << a << ", " << b << ")" << endl;    // 可能有多种输出
}
int main() {
    thread t1(ValueSet, 0);
    thread t2(Observer, 0);
    t1.join();
    t2.join();
    cout << "Got (" << a << ", " << b << ")" << endl;    // Got (1, 2)
}
// 编译选项:g++ -std=c++11 6-3-4.cpp -lpthread

对于上面的代码,比较合理的打印值有:(0,0),(1,0),(1,2),然而在非顺序一致性时可能会打印(0,2)这样的结果。

这是因为对于非顺序一致性场景下,编译器在认定a、b的赋值语句的执行先后顺序对输出结果有任何的影响的话,则可以依情况将指令重排序(reorder)以提高性能。因此就有可能会将b的赋值语句提前到a的赋值语句之前,从而得到(0,2)这样的结果。

当然,默认情况下,在C++11中的原子类型的变量在线程中总是保持着顺序执行的特性(非原子类型则没有必要,因为不需要在线程间进行同步)。我们称这样的特性为“顺序一致”的,即代码在线程中运行的顺序与程序员看到的代码顺序一致,a的赋值语句永远发生于b的赋值语句之前。

2.内存模型

通常情况下,内存模型是一个硬件的概念,表示机器指令以什么样的顺序被执行。

对于"t = 1; a = t; b = 2;"可以用如下的伪汇编表示:

1: Loadi     reg3, 1;     # 将立即数1放入寄存器reg3
2:Move     reg4, reg3;   # 将reg3的数据放入reg4
3: Store     reg4, a;     # 将寄存器reg4中的数据存入内存地址a

4: Loadi     reg5, 2;     # 将立即数2放入寄存器reg5
5: Store     reg5, b;     # 将寄存器reg5中的数据存入内存地址b

通常情况下,应该按照1,2,3,4,5顺序执行,这样的内存模型称为强顺序的。这时a的赋值始终先于b的赋值执行。

但是我们可以看到,指令1,2,3与指令4,5毫无关联,因此一些处理器可能就会重排指令顺序,比如1,4,2,5,3的顺序执行。这种场景,我们称为弱顺序的,b的赋值也就会先于a的赋值。

而在多线程中,强顺序意味着,多个线程看到的指令执行顺序是一致的且反馈到处理器层面,内存数据变换顺序与指令顺序一致,而弱顺序则无法保证这一点。

而原子操作要求都是顺序的,这在强顺序内存模型下是不需要额外处理的,而对于弱顺序内存模型下,则需要添加内存栅栏这样的指令来确保顺序一致性,这对性能往往有较大的损耗。

3.内存顺序memory_order

以上描述的都是硬件上的一些内存模型,而C++11引入的内存模型和顺序一致性则是针对编译器而言:

  • 编译器保证原子操作的指令间顺序不变,即保证产生的读写原子类型的变量的机器指令与代码编写者看到的是一致的。
  • 处理器对原子操作的汇编指令的执行顺序不变。这对于x86这样的强顺序的体系结构而言,并没有任何的问题;而对于PowerPC这样的弱顺序的体系结构而言,则要求编译器在每次原子操作后加入内存栅栏。

C++11的原子操作默认都是顺序一致性的,这对强顺序体系而言没有影响,但是对于弱顺序体系而言,添加内存栅栏来确保顺序一致性,会大大增加性能消耗。为了解决这一问题,C++11引入了内存顺序memory_order的概念,即对所有的原子操作提供一个参数入口,传入不同的momery_order以弱化对顺序一致性的要求。

具体使用如下:

#include <thread>
#include <atomic>
#include <iostream>
using namespace std;
atomic<int> a {0};
atomic<int> b {0};
int ValueSet(int) {
    int t = 1;
    a.store(t, memory_order_relaxed);
    b.store(2, memory_order_relaxed);
}
int Observer(int) {
    cout << "(" << a << ", " << b << ")" << endl;    // 可能有多种输出
}
int main() {
    thread t1(ValueSet, 0);
    thread t2(Observer, 0);
    t1.join();
    t2.join();
    cout << "Got (" << a << ", " << b << ")" << endl;    // Got (1, 2)
    return 0;
}
// 编译选项:g++ -std=c++11 6-3-6.cpp -lpthread

a和b的赋值操作传入参数memory_order_relaxed,表示对执行顺序不做任何要求,从而放开顺序一致性。

memory_order的枚举值有:

通常情况下,我们可以把atomic成员函数可使用的memory_order值分为以下3组:

❑ 原子存储操作(store)可以使用memorey_order_relaxed、memory_order_release、memory_order_seq_cst。

❑ 原子读取操作(load)可以使用memorey_order_relaxed、memory_order_consume、memory_order_acquire、memory_order_seq_cst。

❑ RMW操作(read-modify-write),即一些需要同时读写的操作,比如之前提过的atomic_flag类型的test_and_set()操作。又比如atomic类模板的atomic_compare_exchange()操作等都是需要同时读写的。RMW操作可以使用memorey_order_relaxed、memory_order_consume、memory_order_acquire、memory_order_release、memory_order_acq_rel、memory_order_seq_cst。

排除顺序一致和松散两种方式,我们能不能保证程序“既快又对”地运行呢?实际上,我们所需要的只是a.store先于b.store发生,b.load先于a.load发生的顺序。这要这两个“先于发生”关系得到了遵守,对于整个程序而言来说,就不会发生线程间的错误。所以我们可以修改代码如下:

#include <thread>
#include <atomic>
#include <iostream>
using namespace std;
atomic<int> a;
atomic<int> b;
int Thread1(int) {
    int t = 1;
    a.store(t, memory_order_relaxed);
    b.store(2, memory_order_release); // 本原子操作前所有的写原子操作必须完成
}
int Thread2(int) {
    while(b.load(memory_order_acquire) != 2);   // 本原子操作必须完成才能执行之后所有
    的读原子操作
    cout << a.load(memory_order_relaxed) << endl;    // 1
}
int main() {
    thread t1(Thread1, 0);
    thread t2(Thread2, 0);
    t1.join();
    t2.join();
    return 0;
}
// 编译选项:g++ -std=c++11 6-3-8.cpp -lpthread

b.store采用了memory_order_release内存顺序,这保证了本原子操作前所有的写原子操作必须完成,也即a.store操作必须发生于b.store之前。b.load采用了memory_order_acquire作为内存顺序,这保证了本原子操作必须完成才能执行之后所有的读原子操作。即b.load必须发生在a.load操作之前。这样一来,通过确立“先于发生”关系的,我们就完全保证了代码运行的正确性,即当b的值为2的时候,a的值也确定地为1。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/243865.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

C语言算法~BF算法和KMP算法

各位CSDN的各位你们好啊&#xff0c;今天小赵要给大家分享一个算法方面的知识这个算法也是小赵琢磨了好久&#xff0c;才算把它理明白&#xff0c;今天小赵就用一篇博客带你理明白这个算法——KMP算法。当然再介绍这个算法前&#xff0c;小赵还会介绍一个BF算法和一个函数&…

对多个 App 设计工具组件使用一个回调

当要在App 中提供多种方法来执行某个操作时&#xff0c;在组件间共享回调非常有用。例如&#xff0c;当用户点击按钮或在编辑字段中按下 Enter 键时&#xff0c;App 可以用同样的方式响应。 共享回调的示例 此示例说明如何创建一个 App&#xff0c;其中包含共享一个回调的两个…

数字孪生博物馆解决方案

数字孪生技术在博物馆领域的应用&#xff0c;可以为博物馆提供更丰富的数字化体验&#xff0c;促进文物的保护、展示和教育。以下是数字孪生博物馆解决方案的一些关键组成部分&#xff0c;希望对大家有所帮助。北京木奇移动技术有限公司&#xff0c;专业的软件外包开发公司&…

vue echart实现横向柱状图颜色渐变、标签右对齐

需求&#xff1a;用echart实现柱状图的横向展示&#xff0c;对指定数据的柱状图进行颜色区分&#xff0c;且对应标签值展示在柱状图右侧&#xff0c;实现文字的右对齐。 主要问题点&#xff1a; 1、柱状图的颜色渐变 通过colorStops设置color渐变的起止颜色&#xff0c; color…

在Linux上安装配置Nginx高性能Web服务器

1 前言 Nginx是一个高性能的开源Web服务器&#xff0c;同时也可以作为反向代理服务器、负载均衡器、HTTP缓存以及作为一个邮件代理服务器。它以其出色的性能和灵活性而闻名&#xff0c;被广泛用于处理高流量的网站和应用程序。本文将介绍在Linux环境中安装Nginx的步骤&#xf…

josef约瑟 静态电压继电器 HWY-41B 19-240V 导轨式安装

HWY-40系列无辅源静态电压继电器 HWY-41A无辅源静态电压继电器 HWY-42A无辅源静态电压继电器 HWY-43A无辅源静态电压继电器 HWY-44A无辅源静态电压继电器 HWY-45A无辅源静态电压继电器 HWY-41B无辅源静态电压继电器 HWY-42B无辅源静态电压继电器 HWY-43B无辅源静态电压继电器 …

【项目管理】CMMI对项目管理有哪些个人启发和思考

导读&#xff1a;本人作为项目经理参与公司CMMI5级评审相关材料准备工作&#xff0c;现梳理CMMI有关知识点&#xff0c;并结合项目给出部分示例参考&#xff0c;以及本人对于在整理材料过程中一些启发和体验思考。 目录 1、CMMI定义 2、CMMI-5级 3、CMMI文档清单 4、示例-度…

多表查询、事务、索引

目录 数据准备 分类 内连接 外连接 子查询 事务 四大特性 索引 数据准备 SQL脚本&#xff1a; #建议&#xff1a;创建新的数据库 create database db04; use db04;-- 部门表 create table tb_dept (id int unsigned primary key auto_increment comment 主键…

如何制作安装“易读、易懂、易操作”的电子版说明书

在当今的数字化时代&#xff0c;电子版说明书已经不再是单纯的技术文档。对于大多数用户来说&#xff0c;电子说明书是他们接触产品或服务的第一个触点&#xff0c;它直接影响到用户对产品或服务的初步印象和后续使用体验。那么&#xff0c;如何制作安装一份“易读、易懂、易操…

基于CNN+数据增强+残差网络Resnet50的少样本高准确度猫咪种类识别—深度学习算法应用(含全部工程源码)+数据集+模型(一)

系列文章目录 基于CNN数据增强残差网络Resnet50的少样本高准确度猫咪种类识别—深度学习算法应用(含全部工程源码)数据集模型&#xff08;一&#xff09; 基于CNN数据增强残差网络Resnet50的少样本高准确度猫咪种类识别—深度学习算法应用(含全部工程源码)数据集模型&#xf…

想速成硬件工程师?请先学这50个电路

要说在电子工程师所有分类里&#xff0c;哪个岗位技术含量极高且不易被淘汰&#xff1f;那毫无疑问自然是硬件工程师&#xff0c;虽然工资略低于软件工程师&#xff0c;但技术在手&#xff0c;永远不怕没饭碗&#xff0c;所以越来越多人选择成为硬件工程师&#xff0c;那么想要…

华为交换机——配置策略路由(基于IP地址)示例

一、组网需求&#xff1a; 汇聚层Switch做三层转发设备&#xff0c;接入层设备LSW做用户网关&#xff0c;接入层LSW和汇聚层Switch之间路由可达。汇聚层Switch通过两条链路连接到两个核心路由器上&#xff0c;一条是高速链路&#xff0c;网关为10.1.20.1/24&#xff1b;另外一…

智能部署之巅:Amazon SageMaker引领机器学习革新

本篇文章授权活动官方亚马逊云科技文章转发、改写权&#xff0c;包括不限于在 亚马逊云科技开发者社区, 知乎&#xff0c;自媒体平台&#xff0c;第三方开发者媒体等亚马逊云科技官方渠道。 &#xff08;全球TMT2023年12月6日讯&#xff09;亚马逊云科技在2023 re:Invent全球大…

动画制作与动画控制器的使用_unity基础开发教程

动画制作与动画控制器的使用 导入素材创建动画控制器制作人物动画 前面我们讲过2D游戏中环境地图的制作&#xff0c;这里教大家使用动画控制器的使用 导入素材 先导入一下素材 选择window&#xff0c;点击Asset Store 点击Search online 搜索栏输入Sunny&#xff0c;然后回车…

qt 标准对话框的简单介绍

qt常见的标准对话框主要有,标准文件对话框QFileDialog,标准颜色对话框QColorDialog,标准字体对话框QFontDialog,标准输入对话框QInputDialog,标准消息框QMessageBox...... 1. 标准文件对话框QFileDialog,使用函数getOpenFileName()获取用户选择的文件. //qt 函数getOpenFileN…

【QT 5 调试软件+Linux下调用脚本shell-经验总结+初步调试+基础样例】

【QT 5 调试软件Linux下调用脚本shell-经验总结初步调试基础样例】 1、前言2、实验环境3、自我总结4、实验过程&#xff08;1&#xff09;准备工作-脚本1&#xff09;、准备工作-编写运行脚本文件2&#xff09;、给权限3&#xff09;、运行脚本 &#xff08;2&#xff09;进入q…

学习openAI 短长期AGI计划、使命、宪章、开创性研究、产品、工作待遇等

网站的设计&#xff1a;简洁而现代 主页 使命&#xff1a;Creating safe AGI that benefits all of humanity. &#xff08;比人类更聪明的人工智能系统&#xff09;&#xff08;自己实现或帮别人实现都认为是达成使命&#xff09;&#xff08;造福全人类&#xff1a;最大限…

windows任务计划的创建、导出和导入

创建任务计划 任务名称 任务触发器 执行bat的话起始于必须填写 创建成功 导出任务计划 选择导出路径 导出成功 导入任务计划 可视化界面导入任务计划 选择任务计划的xml文件 点击确定 导入成功 命令行导入计划任务 cd /d D:\迅雷下载schtasks.exe /create /tn 1234 /xml 123…

Ubuntu 22.04 Tesla V100s显卡驱动,CUDA,cuDNN,MiniCONDA3 环境的安装

今天来将由《蓝创精英团队》带来一个Ubuntu 显卡环境的安装&#xff0c;主要是想记录下来&#xff0c;方便以后快捷使用。 主要的基础环境 显卡驱动 (nvidia-smi)CUDA (nvidia-smi 可查看具体版本)cuDNN (cuda 深度学习加速库)Conda python环境管理(Miniconda3) Nvidia 驱动…

yolov8实时推理目标识别、区域分割、姿态识别 Qt GUI

介绍一个GUI工具&#xff0c;可以实时做yolov8模型推理&#xff0c;包括目标检测、姿态识别、跟踪、区域分割等操作。 可以接入图像、视频或者RTSP视频流进行验证。 推理模型用的是yolov8转onnx之后的。用ultralytics自带的转换即可&#xff0c;不用带NMS。 框架用的是Qt 任…