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();	//设为truethread 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中的数据存入内存地址a4: 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。

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

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

深度学习中的13种概率分布

1 概率分布概述 共轭意味着它有共轭分布的关系。 在贝叶斯概率论中&#xff0c;如果后验分布 p&#xff08;θx&#xff09;与先验概率分布 p&#xff08;θ&#xff09;在同一概率分布族中&#xff0c;则先验和后验称为共轭分布&#xff0c;先验称为似然函数的共轭先验。 多…

nodejs使用express框架启动服务操作mysql数据库

描述: 首先在本地搭建mysql数据库,配置:host: ‘192.168.3.249’,user: ‘mkx’,password: ‘123456’,database: ‘gg’.测试连接正常.使用express写两个接口, 1.查询所有学生的接口,使用的get请求,无参数. 2.插入一条学生信息,使用post请求,body是一个json的学生信息{name:“…

Java基础课的中下基础课04

目录 二十三、集合相关 23.1 集合 &#xff08;1&#xff09;集合的分支 23.2 List有序可重复集合 &#xff08;1&#xff09;ArrayList类 &#xff08;2&#xff09;泛型 &#xff08;3&#xff09;ArrayList常用方法 &#xff08;4&#xff09;Vector类 &#xff08;…

【产品】Axure的基本使用(二)

文章目录 一、元件基本介绍1.1 概述1.2 元件操作1.3 热区的使用 二、表单型元件的使用2.1 文本框2.2 文本域2.3 下拉列表2.4 列表框2.5 单选按钮2.6 复选框2.7 菜单与表格元件的使用 三、实例3.1 登录2.2 个人简历 一、元件基本介绍 1.1 概述 在Axure RP中&#xff0c;元件是…

Java网络编程-深入理解BIO、NIO

深入理解BIO与NIO BIO BIO 为 Blocked-IO&#xff08;阻塞 IO&#xff09;&#xff0c;在 JDK1.4 之前建立网络连接时&#xff0c;只能使用 BIO 使用 BIO 时&#xff0c;服务端会对客户端的每个请求都建立一个线程进行处理&#xff0c;客户端向服务端发送请求后&#xff0c;…

用print太慢了!强烈推荐这款Python Debug工具~

作为程序员&#xff0c;我们都深知调试&#xff08;Debug&#xff09;在编程过程中的重要性。然而&#xff0c;使用传统的"print"语句进行调试可能效率较低&#xff0c;今天&#xff0c;笔者将推荐一款独具一格的Python调试工具——Reloadium。Reloadium为IDE添加了热…

SU渲染受到电脑性能影响大吗?如何提高渲染速度

一般3d设计师们在进行设计工作前都需要提供一台高配电脑&#xff0c;那么你这知道su渲染对电脑要求高吗&#xff1f;电脑带不动su怎么解决&#xff1f;su对电脑什么配件要求高&#xff1f;今天这篇文章就详细为大家带来电脑硬件对su建模渲染的影响&#xff0c;以及su渲染慢怎么…

基于YOLOv8深度学习的西红柿成熟度检测系统【python源码+Pyqt5界面+数据集+训练代码】目标检测、深度学习实战

《博主简介》 小伙伴们好&#xff0c;我是阿旭。专注于人工智能、AIGC、python、计算机视觉相关分享研究。 ✌更多学习资源&#xff0c;可关注公-仲-hao:【阿旭算法与机器学习】&#xff0c;共同学习交流~ &#x1f44d;感谢小伙伴们点赞、关注&#xff01; 《------往期经典推…

【docker】镜像使用(Nginx 示例)

查看本地镜像列表 docker images删除本地镜像 # docker rmi [容器 ID]docker rmi a6bd71f48f68 查找镜像 docker search nginx 参数介绍 NAME: 镜像仓库源的名称DESCRIPTION: 镜像的描述OFFICIAL: 是否 docker 官方发布STARS: 点赞、喜欢AUTOMATED: 自动构建。 拉去镜像 …

Ubuntu宝塔面板本地部署Emlog个人博客网站并远程访问【内网穿透】

文章目录 前言1. 网站搭建1.1 Emolog网页下载和安装1.2 网页测试1.3 cpolar的安装和注册 2. 本地网页发布2.1 Cpolar临时数据隧道2.2.Cpolar稳定隧道&#xff08;云端设置&#xff09;2.3.Cpolar稳定隧道&#xff08;本地设置&#xff09; 3. 公网访问测试总结 前言 博客作为使…

Centos7防火墙及端口开启

1、防火墙 1.1、查看防火墙是否开启 systemctl status firewalld 1.2、开启防火墙 firewall-cmd --list-ports 1.3、重启防火墙 firewall-cmd --reload 2、端口 2.1、查看所有已开启的端口号 firewall-cmd --list-ports 2.2、手动开启端口 启动防火墙后&#xff0c;默认没有开…

保姆级 | XSS Platform环境搭建

0x00 前言 XSS Platform 平台主要是用作验证跨站脚本攻击。该平台可以部署在本地或服务器环境中。我们可以使用 XSS Platfrom 平台搭建、学习或验证各种类型的 XSS 漏洞。 0x01 环境说明 HECS(云耀云服务器)xss platformUbuntu 22.04Nginx 1.24.0MySQL 5.6.51Pure-Ftpd 1.0.49…

Java实现机考程序界面

机考界面如下&#xff08;单选题&#xff09;&#xff0c;上方是题目状态&#xff0c;下方是题目&#xff0c;1/5/1是已做题目数量、总共题目数量和答对题目数量。 再看一下多选题的界面。 判断题的界面。 回答正确时的反馈&#xff0c;会给出用时。 回答错误时的反馈&#xff…

飞天使-linux操作的一些技巧与知识点4-ansible常用的技巧,配置等

文章目录 ansible配置文件的优先级尝试开始进行操作ansible常用模块ansible 的playbook示例安装phpplaybook中变量的引用 ansible yum install -y ansible 测试是否可用 ansible localhost -m ping /etc/ansible/ansible.cfg &#xff1a;主配置文件&#xff0c;配置 ansible…

LeetCode 每日一题 Day 12 (Hard)|| 二维前缀和二维差分

2132. 用邮票贴满网格图 给你一个m x n的二进制矩阵 grid &#xff0c;每个格子要么为 0 &#xff08;空&#xff09;要么为 1 &#xff08;被占据&#xff09;。 给你邮票的尺寸为 stampHeight x stampWidth 。我们想将邮票贴进二进制矩阵中&#xff0c;且满足以下 限制 和 …

如何避免重要文件夹被盗?多种文件夹防盗方法介绍

当我们将重要数据存放在文件夹中时&#xff0c;一定要保护文件夹的安全&#xff0c;避免文件夹被盗。那么&#xff0c;我们该如何避免重要文件夹被盗呢&#xff1f;下面我们就来了解一下。 EFS功能 EFS是Windows提供的数据加密功能&#xff0c;可以加密NTFS卷上的文件和文件夹…

verilog基本语法-case语句-译码电路,编码电路,选择器电路

概述&#xff1a; 本节主要讲解LUT构造的组合逻辑电路中的译码电路&#xff0c;编码电路&#xff0c;选择器电路。这些基本电路是使用的最广泛的电路&#xff0c;但是一般情况下很容易忽略这些电路。其中译码电路是构成RAM中写地址的电路&#xff0c;而选择电路是构成RAM中数据…

java 家教管理系统Myeclipse开发mysql数据库web结构jsp编程计算机网页项目

一、源码特点 java 家教管理系统是一套完善的java web信息管理系统&#xff0c;对理解JSP java编程开发语言有帮助&#xff0c;系统具有完整的源代码和数据库&#xff0c;系统主要采用B/S模式开发。开发环境为 TOMCAT7.0,Myeclipse8.5开发&#xff0c;数据库为Mysql5.0&…

代码随想录刷题题Day13

刷题的第十三天&#xff0c;希望自己能够不断坚持下去&#xff0c;迎来蜕变。&#x1f600;&#x1f600;&#x1f600; 刷题语言&#xff1a;C Day13 任务 ● 104.二叉树的最大深度 559.n叉树的最大深度 ● 111.二叉树的最小深度 ● 222.完全二叉树的节点个数 1 二叉树的最大…

Linux centos7 添加自定义服务(frps服务)

文中以frps为例创建frp服务端的服务 1、创建服务文件 vi /etc/systemd/system/frps.service 注意&#xff1a;文件名frps就是服务名称 2、编辑服务文件内容 [Unit] # 服务名称&#xff0c;可自定义 Description frp server After network.target syslog.target Wants n…