当前位置: 首页 > news >正文

C++11中的std::atomic保证的原子性是什么

文章目录

  • 前言
  • C++中的atomic
  • 一个简单的自增运算
  • 通过加锁把自增变为原子操作
  • 使用atomic来保证自增的原子性
  • 总结

前言

提到atomic这个词,你首先想到的是什么呢?作为一个长时间混迹于编程世界的菜鸟,我首先想到的一个词是“原子性”,接着飞入脑海的是 “ACID” 这个缩写词组,既然提到了 ACID 我们就来简单的复习一下。

ACID 是指事务管理的4个特性,常见于数据库操作管理中,它们分别是:原子性,一致性,隔离性和持久性。

  • 原子性(Atomicity)是指事务是一个不可分割的工作单位,事务中的操作要么都执行,要么都不执行。
  • 一致性(Consistency)是指事务前后数据的完整性必须保持一致,完全符合逻辑原运算。
  • 隔离性(Isolation)是指在多个用户并发访问数据库时,数据库为每一个用户开启的事务,不能被其他事务的操作数据所干扰,多个并发事务之间要相互隔离,无需感知其他事务的存在。
  • 持久性(Durability)是指一个事务一旦被提交,它对数据库中数据的改变就是永久性的,接下来即使数据库发生故障也不应该对数据造成损坏。

C++中的atomic

原子(atom)是在化学反应中不可分割基本微粒,而编程世界中的原子性也是取自这里的不可分割的含义,不可分割与事务管理中的原子性含义一致,指的是一个操作或者一系列操作只能全都执行或者都不执行,不会只执行其中一部分,那么C++11中引入atomic有什么用?不使用atomic能不能保证原子性呢?

其实C++11中引入atomic主要还是降低了编程的复杂度,如果不使用atomic同样可以使用锁机制来保证原子性,接下来我们来看看为什么需要原子性。

一个简单的自增运算

i++ 是个再简单不过的语句了,我们可以使用它来做一个计数器,每次自增加1,假设我们有一个工程项目有两条商品生产的流水线,每个流水线生产出一件商品则需要计数器加1,这时我们用两个线程来模拟两条流水线,每个线程函数来调用自增的计数器,来看看有什么问题?

#include <iostream>
#include <thread>

int i = 0;

void func(int n)
{
    for (int k = 0; k < n; k++) i++;
}

int main(int argc, char* argv[])
{
    int n = argc > 1 ? atoi(argv[1]) : 100;
    
    std::thread t1(func, n);
    std::thread t2(func, n);
    
    t1.join();
    t2.join();
    
    std::cout << "i=" << i << std::endl;
}

测试代码如上所示,执行 g++ -std=c++20 -O0 -pthread main.cpp && ./a.out 10 命令编译并运行得到结果 i=20,貌似很正常,一共两个线程,每个线程执行10次自增操作,结果就应该是20啊,先别太早下结论,增大自增范围试试。

执行 g++ -std=c++20 -O0 -pthread main.cpp && ./a.out 100000 得到结果 i=112831,多次执行发现每次运行结果都不太一样,但是数据范围在 100000~200000,这就有些奇怪了,每个线程执行循环执行一条语句,那么程序结果应该等于 2n 才对,为什么结果总是小于 2n 呢,难道有些循环没有执行?

其实不是这样的,i++从C++语言的层面来看确实是一条语句,但是真正再和机器打交道时一般会解释成类似于下面这样3条汇编指令:

        // x86 msvc v19.latest
        mov     eax, DWORD PTR _i$[ebp]
        add     eax, 1
        mov     DWORD PTR _i$[ebp], eax

3条指令的含义可以理解为读取、自增,设置共三步,既然不是真正的一条语句,那么在多线的环境下就会生语句的交叉执行,比如第一个线程执行读取变量i的值之后,第二个线程也读取了变量i的值,这样两个线程都进行后续的自增和设置指令后,会发现比预期的值少了一个,这种情况在循环次数较多时尤为明显。

通过加锁把自增变为原子操作

既然每个自增操作可能会被分解成3条指令,那么我们可以加锁来将3条指令捆绑,当一个线程执行自增操作时加锁来防止其他进程“捣乱”,具体修改如下,可以在自增操作前直接加锁:

#include <iostream>
#include <thread>
#include <chrono>
#include <mutex>

int i = 0;
std::mutex mt;

void inc()
{
    std::lock_guard<std::mutex> l(mt);
    i++; 
}

void func(int n)
{
    for (int k = 0; k < n; k++) inc();
}

int main(int argc, char* argv[])
{
    int n = argc > 1 ? atoi(argv[1]) : 100;
    std::chrono::steady_clock::time_point start = std::chrono::steady_clock::now();
    
    std::thread t1(func, n);
    std::thread t2(func, n);
    
    t1.join();
    t2.join();
    
    std::cout << "i=" << i << std::endl;
    std::chrono::duration<double> duration_cost = std::chrono::duration_cast<
        std::chrono::duration<double> >(std::chrono::steady_clock::now() - start);
    std::cout <<  "total cost " << duration_cost.count() << " seconds." << std::endl;
    
    return 0;
}

执行 g++ -std=c++20 -O0 -pthread main.cpp && ./a.out 10000000 命令后运行结果如下:

i=20000000
total cost 2.39123 seconds.

通过加锁,我们已经保证了结果的正确性,但是我们知道加锁的额外消耗还是很大的,有没有其他的方式来实现原子操作呢?

使用atomic来保证自增的原子性

其实在C++11之前可以通过嵌入汇编指令来实现,不过自从C++11引入atomic之后,类似的需求变得简单了许多,可以直接使用autmic这个模板类来实现,代码几乎不需要修改,只需将变量 i 改为 atomic<int> 类型,再把锁去掉就可以了,修改后的代码如下:

#include <iostream>
#include <thread>
#include <chrono>
#include <mutex>
#include <atomic>

std::atomic<int> i = 0; // int -> atomic<int>
std::mutex mt;

void inc()
{
    //std::lock_guard<std::mutex> l(mt);  //remove lock
    i++; 
}

void func(int n)
{
    for (int k = 0; k < n; k++) inc();
}

int main(int argc, char* argv[])
{
    int n = argc > 1 ? atoi(argv[1]) : 100;
    std::chrono::steady_clock::time_point start = std::chrono::steady_clock::now();
    
    std::thread t1(func, n);
    std::thread t2(func, n);
    
    t1.join();
    t2.join();
    
    std::cout << "i=" << i << std::endl;
    std::chrono::duration<double> duration_cost = std::chrono::duration_cast<
        std::chrono::duration<double> >(std::chrono::steady_clock::now() - start);
    std::cout <<  "total cost " << duration_cost.count() << " seconds." << std::endl;
    
    return 0;
}

执行 g++ -std=c++20 -O0 -pthread main.cpp && ./a.out 10000000 命令后运行结果如下:

i=20000000
total cost 1.6554 seconds.

通过对比可以发现,使用 std::atomic 模板类之后,在保证了结果正确的同时,相比于加锁实现原子性速度上有了明显的提升。

总结

  • ACID 是指事务管理中的原子性,一致性,隔离性和持久性4个特性。
  • 加锁(写锁)的目的通常是将可能同时发生的操作串行化,以此来避免对资源的竞争出现问题
  • 操作的并行加快了任务的处理速度,而“加锁”使部分操作回归到串行,两者相互配合是为了在更短的时间内得到正确的结果
  • std::atomic 降低了原子性操作编程的难度,同时相比于加锁实现原子性还有了性能的提升

==>> 反爬链接,请勿点击,原地爆炸,概不负责!<<==

时光时光慢些吧,不要再让你变老了,我愿用我一切,换你岁月长留~

时间对于每个人来说,都是公平的,真的是这样吗?我觉得未必吧!

相关文章:

  • .bat批处理(十):从路径字符串中截取盘符、文件名、后缀名等信息
  • linux环境下从路径字符串中截取目录和文件名信息
  • MD5是用来加密的吗?BCrypt又是什么呢
  • 树的带权路径长度和哈夫曼树
  • 完全图与强连通图的那些坑
  • linux环境下恢复rm误删的文件
  • 记一次使用Valgrind查找解决内存问题的玄幻旅程
  • 网络工具nc的常见功能和用法
  • git常用配置——git show/diff tab 显示宽度
  • Windows设置防火墙允许指定应用正常使用网络
  • 2021年终总结——脚踏实地,为下一次腾飞积蓄力量
  • 通过WindowsStore安装QuickLook小工具方便文件预览
  • linux环境下随时照看服务器进程的ps和top命令
  • 简单梳理下git的使用感受,思考git中最重要的是什么
  • 总结下各种常见树形结构的定义及特点(二叉树、AVL树、红黑树、Trie树、B树、B+树)
  • create-react-app做的留言板
  • CSS选择器——伪元素选择器之处理父元素高度及外边距溢出
  • isset在php5.6-和php7.0+的一些差异
  • JS字符串转数字方法总结
  • node-glob通配符
  • python 装饰器(一)
  • socket.io+express实现聊天室的思考(三)
  • spring学习第二天
  • 笨办法学C 练习34:动态数组
  • 容器服务kubernetes弹性伸缩高级用法
  • 树莓派 - 使用须知
  • 微信端页面使用-webkit-box和绝对定位时,元素上移的问题
  • 小程序开发中的那些坑
  • 移动端唤起键盘时取消position:fixed定位
  • 用quicker-worker.js轻松跑一个大数据遍历
  • 优秀架构师必须掌握的架构思维
  • 鱼骨图 - 如何绘制?
  • shell使用lftp连接ftp和sftp,并可以指定私钥
  • #ubuntu# #git# repository git config --global --add safe.directory
  • #前后端分离# 头条发布系统
  • (01)ORB-SLAM2源码无死角解析-(56) 闭环线程→计算Sim3:理论推导(1)求解s,t
  • (09)Hive——CTE 公共表达式
  • (2.2w字)前端单元测试之Jest详解篇
  • (7)STL算法之交换赋值
  • (C语言)共用体union的用法举例
  • (done) NLP “bag-of-words“ 方法 (带有二元分类和多元分类两个例子)词袋模型、BoW
  • (初研) Sentence-embedding fine-tune notebook
  • (二)正点原子I.MX6ULL u-boot移植
  • (附源码)计算机毕业设计ssm高校《大学语文》课程作业在线管理系统
  • (蓝桥杯每日一题)平方末尾及补充(常用的字符串函数功能)
  • (三) diretfbrc详解
  • (四)JPA - JQPL 实现增删改查
  • (四)鸿鹄云架构一服务注册中心
  • (转)IIS6 ASP 0251超过响应缓冲区限制错误的解决方法
  • (转)linux自定义开机启动服务和chkconfig使用方法
  • (转载)Google Chrome调试JS
  • .[backups@airmail.cc].faust勒索病毒的最新威胁:如何恢复您的数据?
  • .net 使用$.ajax实现从前台调用后台方法(包含静态方法和非静态方法调用)
  • .NET6 开发一个检查某些状态持续多长时间的类
  • .net对接阿里云CSB服务