————————————————

版权声明:本文为CSDN博主「Dablelv」的原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接及本声明。

原文链接:https://blog.csdn.net/K346K346/article/details/85345477

1.认识原子操作

原子操作是在多线程程序中“最小的且不可并行化的”操作,意味着多个线程访问同一个资源时,有且仅有一个线程能对资源进行操作。通常情况下原子操作可以通过互斥的访问方式来保证,例如Linux下的互斥锁(mutex),Windows 下的临界区(Critical Section)等。下面看一个Linux环境使用 POSIX 标准的 pthread 库实现多线程下的原子操作:

1 #include

2 #include

3 using namespacestd;4

5 int64_t total=0;6 pthread_mutex_t m=PTHREAD_MUTEX_INITIALIZER;7

8 //线程函数,用于累加

9 void* threadFunc(void*args)10 {11 int64_t endNum=*(int64_t*)args;12 for(int64_t i=1;i<=endNum;++i)13 {14 pthread_mutex_lock(&m);15 total+=i;16 pthread_mutex_unlock(&m);17 }18 }19

20 intmain()21 {22 int64_t endNum=100;23 pthread_t thread1ID=0,thread2ID=0;24

25 //创建线程1

26 pthread_create(&thread1ID,NULL,threadFunc,&endNum);27 //创建线程2

28 pthread_create(&thread2ID,NULL,threadFunc,&endNum);29

30 //阻塞等待线程1结束并回收资源

31 pthread_join(thread1ID,NULL);32 //阻塞等待线程2结束并回收资源

33 pthread_join(thread2ID,NULL);34

35 cout<

36 }

上面的代码,两个线程同时对 total 进行操作,为了保证total+=i 的原子性,采用互斥锁来保证同一时刻只有同一线程执行total+=i操作,所以得出正确结果total=10100。如果没有做互斥处理,那么 total 同一时刻可能会被两个线程同时操作,即会出现两个线程同时读取了寄存器中的 total 值,分别操作之后又写入寄存器,这样就会有一个线程的增加操作无效,会得出一个小于 10100 随机的错误值。

2.C++11 实现原子操作

在 C++11 之前,使用第三方 API 可以实现并行编程,比如 pthread 多线程库,但是在使用时需要创建互斥锁,以及进行加锁、解锁等操作来保证多线程对临界资源的原子操作,这无疑增加了开发的工作量。不过从 C++11 开始,C++ 从语言层面开始支持并行编程,内容包括了管理线程、保护共享数据、线程间的同步操作、低级原子操作等各种类。新标准极大地提高了程序的可移植性,以前的多线程依赖于具体的平台,而现在有了统一的接口。

C++11 通过引入原子类型帮助开发者轻松实现原子操作。

1 #include

2 #include

3 #include

4 using namespacestd;5

6 atomic_int64_t total = 0; //atomic_int64_t相当于int64_t,但是本身就拥有原子性7

8 //线程函数,用于累加

9 voidthreadFunc(int64_t endNum)10 {11 for (int64_t i = 1; i <= endNum; ++i)12 {13 total +=i;14 }15 }16

17 intmain()18 {19 int64_t endNum = 100;20 thread t1(threadFunc, endNum);21 thread t2(threadFunc, endNum);22

23 t1.join();24 t2.join();25

26 cout << "total=" << total << endl; //10100

27 }

程序正常编译并运行输出正确结果total=10100。使用C++11提供的原子类型与多线程标准接口,简洁地实现了多线程对临界资源的原子操作。原子类型C++11中通过atomic类模板来定义,比如atomic_int64_t是通过typedef atomic atomic_int64_t实现的,使用时需包含头文件。除了提供atomic_int64_t,还提供了其它的原子类型。常见的原子类型有:

原子操作是平台相关的,原子类型能够实现原子操作是因为 C++11 对原子类型的操作进行了抽象,定义了统一的接口,并要求编译器产生平台相关的原子操作的具体实现。C++11 标准将原子操作定义为 atomic 模板类的成员函数,包括读(load)、写(store)、交换(exchange)等。对于内置类型而言,主要是通过重载一些全局操作符来完成的。比如对上文total+=i的原子加操作,是通过对operator+=重载来实现的。使用g++ 编译的话,在 x86_64 的机器上,operator+=() 函数会产生一条特殊的以 lock 为前缀的 x86_64 指令,用于控制总线及实现 x86_64平台上的原子性加法。

有一个比较特殊的原子类型是 atomic_flag,因为 atomic_flag 与其他原子类型不同,它是无锁(lock_free)的,即线程对其访问不需要加锁,而其他的原子类型不一定是无锁的。因为atomic并不能保证类型T是无锁的,另外不同平台的处理器处理方式不同,也不能保证必定无锁,所以其他的类型都会有 is_lock_free() 成员函数来判断是否是无锁的。atomic_flag 只支持 test_and_set() 以及 clear() 两个成员函数,test_and_set()函数检查 std::atomic_flag 标志,如果 std::atomic_flag 之前没有被设置过,则设置 std::atomic_flag 的标志;如果之前 std::atomic_flag 已被设置,则返回 true,否则返回 false。clear()函数清除 std::atomic_flag 标志使得下一次调用 std::atomic_flag::test_and_set()返回 false。可以用 atomic_flag 的成员函数test_and_set() 和 clear() 来实现一个自旋锁(spin lock):

1 #include

2 #include

3 #include

4 #include

5

6 std::atomic_flag lock =ATOMIC_FLAG_INIT;7

8 voidfunc1()9 {10 while (lock.test_and_set(std::memory_order_acquire)) //在主线程中设置为true,需要等待t2线程clear

11 {12 std::cout << "func1 wait" <<:endl std::cout do something>

17 voidfunc2()18 {19 std::cout << "func2 start" <<:endl lock.clear>

23 intmain()24 {25 lock.test_and_set(); //设置状态

26 std::thread t1(func1);27 usleep(1); //睡眠1us

28 std::thread t2(func2);29

30 t1.join();31 t2.join();32

33 return 0;34 }

以上代码中,定义了一个 atomic_flag 对象 lock,使用初始值 ATOMIC_FLAG_INIT 进行初始化,即处于 false 的状态。线程 t1 调用 test_and_set() 一直返回 true(因为在主线程中被设置过),所以一直在等待,而等待一段时间后当线程 t2 运行并调用了 clear(),test_and_set() 返回了 false 退出循环等待并进行相应操作。这样一来,就实现了一个线程等待另一个线程的效果。当然,可以封装成锁操作的方式,比如:

1 void Lock(atomic_flag& lock){ while ( lock.test_and_set()); }2 void UnLock(atomic_flag& lock){ lock.clear(); }

3.内存模型:强顺序与弱顺序

内存模型通常是硬件上的概念,表示的是机器指令是以什么样的顺序被处理器执行的,现代的处理器并不是逐条处理机器指令的:

1 1: Load reg3, 1; //将立即数1放入寄存器reg3

2 2: Move reg4,reg3; //将reg3的数据放入reg4

3 3: Store reg4, a; //将reg4的数据存入内存地址a

4 4: Load reg5, 2; //将立即数2放入寄存器reg5

5 5: Store reg5, b; //将reg5的数据存入内存地址b

以上的伪汇编代码代表了temp = 1; a = temp; b = 2,通常情况下指令都是按照1~5的顺序执行,这种内存模型称为强顺序(strong ordered)。不过可以看到,指令1、2、3和指令4、5的运行顺序不影响结果,有一些处理器可能会将指令的顺序打乱,例如按照1-4-2-5-3的顺序执行,这种内存模型称为弱顺序(weak ordered)。弱顺序内存模型下,指令5(b的赋值)很有可能在指令3(a的赋值)之前完成。

现实中,x86_64以及SPARC(TSO模式)都是采用强顺序内存模型的平台。在多线程程序中,强顺序类型意味着对于各个线程看到的指令执行顺序是一致的。对于处理器而言,内存中的数据被改变的顺序与机器指令中的一致。相反的,弱顺序就是各个线程看到的内存数据被改变的顺序与机器指令中声明的不一致。弱顺序内存模型可能会导致程序问题,为什么有些平台,诸如Alpha、PowerPC、Itanlium、ArmV7等平台会使用这种模型?简单地说,这种模型能让处理器有更好的并行性,提高指令执行的效率。并且,为了保证指令执行的顺序,通常需要在汇编指令中加入一条内存栅栏(memory barrier)指令,但是会影响处理器性能。比如在PowerPC上,就有一条名为sync的内存栅栏指令。该指令迫使已经进入流水线中的指令都完成后处理器才会执行sync以后的指令。

事实上,C++11中的原子操作还可以包含一个参数:内存顺序(memory_order),是C++11为原子类型定义的内存模型,让程序员根据实际情况灵活地控制原子类型的执行顺序。通常情况下,使用该参数将有利于编译器进一步提高并行性能。

1 atomic a{0};2 atomic b{0};3

4 //线程函数

5 int valueSet(int)6 {7 int t=1;8 a.store(t);9 b.store(2);10 }

如果原子类型变量a和b并没有要求执行的顺序性,那么可以采用一种松散的内存模型来放松对原子操作的执行顺序的要求。改造如下:

1 voidfunc1()2 {3 int t=t;4 a.store(t, std::memory_order_relaxed);5 b.store(2, std::memory_order_relaxed);6 }

上面的代码使用了store函数进行赋值,store函数接受两个参数,第一个是要写入的值,第二个是名为memory_order的枚举值。这里使用了std::memory_order_relaxed,表示松散内存顺序,该枚举值代表编译器可以任由编译器重新排序或则由处理器乱序处理。这样a和b的赋值执行顺序性就被解除了。在C++11中一共有7种memory_order枚举值,默认按照memory_order_seq_cst执行:

需要注意的是,不是所有的memory_order都能被atomic成员使用:

(1)store函数可以使用memory_order_seq_cst、memory_order_release、memory_order_relaxed。

(2)load函数可以使用memory_order_seq_cst、memory_order_acquire、memory_order_consume、memory_order_relaxed。

(3)需要同时读写的操作,例如test_and_flag、exchange等操作。可以使用memory_order_seq_cst、memory_order_release、memory_order_acquire、memory_order_consume、memory_order_relaxed。

原子类型提供的一些操作符,比如operator=、operator+=等函数,都是以memory_order_seq_cst为memory_order的实参的原子操作封装,所以他们都是顺序一致性的。如果要指定内存顺序的话,则应该采用store、load、atomic_fetch_add这样的版本。

最后说明一下,std::atomic和std::memory_order只有在多线程无锁编程时才会用到。在x86_64平台,由于是强顺序内存模型的,为了保险起见,不要使用std::memory_order,使用std::atmoic默认形式即可,因为std::atmoic默认是强顺序内存模型。

参考文献

[1]《深入理解C++11》笔记-原子类型和原子操作

[2] 深入理解C++11[M].C6.3原子类型与原子操作.P196-214

c++ 原子操作 赋值_C++11 多线程中原子类型与原子操作相关推荐

  1. C++11 原子类型与原子操作

    文章目录 1.认识原子操作 2.C++11 实现原子操作 3.内存模型:强顺序与弱顺序 参考文献 1.认识原子操作 原子操作是在多线程程序中"最小的且不可并行化的"操作,意味着多个 ...

  2. c++ 原子操作 赋值_5.2 C++中的原子操作和原子类型

    5.2 C++中的原子操作和原子类型 原子操作 是个不可分割的操作. 在系统的所有线程中,你是不可能观察到原子操作完成了一半这种情况的: 它要么就是做了,要么就是没做,只有这两种可能. 如果从对象读取 ...

  3. C++11多线程中std::call_once的使用

    C++11中的std::call_once函数位于<mutex>头文件中. 在多线程编程中,有时某个任务只需要执行一次,此时可以用C++11中的std::call_once函数配合std: ...

  4. Multi-thread--C++11多线程中std::call_once的使用

    C++11中的std::call_once函数位于<mutex>头文件中. 在多线程编程中,有时某个任务只需要执行一次,此时可以用C++11中的std::call_once函数配合std: ...

  5. 结构体里有指针 scanf赋值_C++|链表中常见的链表节点指针操作

    链表节点指针做左值时的赋值可以理解为左值指向或链接. 1 用带指针的结构体类型来描述链接结点 typedef struct Lnode{ ElemType data; /*数据域,保存结点的值 */s ...

  6. 多线程中的使用共享变量的问题

    一组并发线程运行在一个进程的上下文中,每个线程都有它自己独立的线程上下文,例如:栈.程序计数器.线程ID.条件码等,每个线程和其它的线程一起共享除此之外的进程上下文的剩余部分,包括整个用户的虚拟地址空 ...

  7. 第5章 C++内存模型和原子类型操作

    第5章 C++内存模型和原子类型操作 本章主要内容 ※ C++11内存模型详解 ※ 标准库提供的原子类型 使用各种原子类型 ※ 原子操作实现线程同步功能 C++11标准中,有一个十分重要特性,常被程序 ...

  8. Rust原子类型和内存排序

    简介 原子类型在构建无锁数据结构,跨线程共享数据,线程间同步等多线程并发编程场景中起到至关重要的作用.本文将从Rust提供的原子类型和原子类型的内存排序问题两方面来介绍. Rust原子类型 Rust标 ...

  9. cpp 原子操作_C++ 新特性学习(八) — 原子操作和多线程库[多工内存模型]

    这是我对C++新特性系统学习的最后一部分,之后就靠实践中再来看新标准的新特性啦. 在之前,我对这部分没太在意,直到看到了一篇文章 http://blog.csdn.net/pongba/article ...

  10. C++11开发中的Atomic原子操作

    C++11开发中的Atomic原子操作 Nicol的博客铭 原文  https://taozj.org/2016/09/C-11%E5%BC%80%E5%8F%91%E4%B8%AD%E7%9A%84 ...

最新文章

  1. 核心交换机的链路聚合、冗余、堆叠、热备份
  2. bat-bat-bat (重要的事情说三遍)
  3. tfidf关键词提取_基于TextRank提取关键词、关键短语、摘要,文章排序
  4. Linux进阶之软件管理
  5. python t t_Python ttable包_程序模块 - PyPI - Python中文网
  6. 老李分享:持续集成学好jenkins之Git和Maven配置
  7. loadrunner可用许可证
  8. 【ACM ICPC 2011–2012, Northeastern European Regional Contest】Interactive Permutation Guessing【交互题】
  9. c语言判断sjis编码,loadrunner Web_类函数之web_sjis_to_euc_param()
  10. 非线性系统离散线性化方法(一)
  11. 初探flask debug生成pin码
  12. MATLAB学习系列--绘制函数曲线
  13. 游戏账号交易平台,是专门为网络游戏提供相关交易服务的电子商务平台,主要从事网络游戏账号的交易。
  14. 方兴未艾的CORBA
  15. 数据挖掘从入门到放弃(一):线性回归和逻辑回归
  16. iOS调优 | 深入理解Link Map File
  17. java 气泡图_java报表开发制作气泡图
  18. python运动学仿真的意义_运动学仿真和动力学仿真有什么区别和联系?
  19. 巴别塔合约作战终端开发日记3——服务器负载优化
  20. 阿里云NLP接口调用

热门文章

  1. Peak CAN与Matlab建立通信 Simulink仿真接收CAN报文
  2. 链接世界•缔造传奇 世界区块链之都(筹)成立大会召开在即
  3. 弗洛伊德算法三重循环的最通俗理解
  4. 浙江大学 PTA C语言-实验8.2 指针与字符串 7-2 字符串排序
  5. 转载《计算机科学返思录》
  6. 2022-2027年中国网络财经信息服务行业发展监测及投资战略研究报告
  7. 设计模式(1)-单例模式 及使用该模式改造的日志写入代码
  8. python学习之美多商城(十七):商品部分:商品搜索、Elasticsearch搜索引擎(Docker部署及haystack对接)
  9. latex转换pdf 出错解决: IEEEtran.cls not found
  10. 摄影DIY 用鞋盒制作简易大画幅相机