目录
(2)compare_exchange_weak()和compare_exchange_strong()
并发:一个处理器上跑一个进程(多个线程),轮流启用时间片。
并行:多个处理器同时各跑一个线程/进程。
生动讲解可看当我们在说“并发、多线程”,说的是什么? - 知乎 (zhihu.com)
所谓的原子操作,取的就是“原子是最小的、不可分割的最小个体”的意义,它表示在多个线程访问同一个全局资源的时候,能够确保所有其他的线程都不在同一时间内访问相同的资源。也就是他确保了在同一时刻只有唯一的线程对这个资源进行访问。这有点类似互斥对象对共享资源的访问的保护,但是原子操作更加接近底层,因而效率更高。
在以往的C++标准中并没有对原子操作进行规定,我们往往是使用汇编语言,或者是借助第三方的线程库,例如intel的pthread来实现。在新标准C++11,引入了原子操作的概念,并通过这个新的头文件提供了多种原子操作数据类型,例如,atomic_bool,atomic_int等等,如果我们在多个线程中对这些类型的共享资源进行操作,编译器将保证这些操作都是原子性的,也就是说,确保任意时刻只有一个线程对这个资源进行访问,编译器将保证,多个线程访问这个共享资源的正确性。从而避免了锁的使用,提高了效率。
atomic实现了C++标准定义的原⼦操作库(C++11.29),它封装了不同计算机硬件的底层操作原语,提供了跨平台的原⼦操作功能,让我们完全摆脱了并发竞争读写变量的困扰。
atomic库定义了基本模板类atomic<T>, atomic可以把对类型T的操作原⼦化,但不是任意的类型都可以原⼦化的,类型T如下:
atomic<T>还针对整数类型和指针类型进⾏了特化,增加了⼀些特殊的操作:fetch_xxx。
为了⽅便使⽤,atomic库还为这些原⼦化的整数和指针类型定义了typedef,它们的名字都以 “atomic_”为前缀(注意没有float和double)。
typedef atomic< char > atomic_char;
typedef atomic< unsigned char > atomic_uchar;
typedef atomic< signed char > atomic_schar;
typedef atomic< uint8_t > atomic_uint8_t;
typedef atomic< int8_t > atomic_int8_t;
typedef atomic< unsigned short > atomic_ushort;
typedef atomic< short > atomic_short;
typedef atomic< uint16_t > atomic_uint16_t;
typedef atomic< int16_t > atomic_int16_t;
typedef atomic< unsigned int > atomic_uint;
typedef atomic< int > atomic_int;
typedef atomic< uint32_t > atomic_uint32_t;
typedef atomic< int32_t > atomic_int32_t;
typedef atomic< unsigned long > atomic_ulong;
typedef atomic< long > atomic_long;
typedef atomic< uint64_t > atomic_uint64_t;
typedef atomic< int64_t > atomic_int64_t;
...
atomic的⼤部分成员函数返回的都不是左值,这意味着它与⾮原⼦化的类型有明显的不同。
有两种⽅式创建atomic对象:
#include <atomic>
//标准库可以使用atomic_init来初始化
std::atomic<int> a;
std::atomic_init<int>(&a, 10);
atomic最重要的两个成员函数是store()和load(),它们以原⼦的⽅式存取atomic内部的值,不会因并发访问导致数据不⼀致。atomic还使⽤操作符重载简化了store()和load()的调⽤⽅式:赋值等价于store(),⽽隐式类型转换则等价于load()。
#include <boost/atomic.hpp>
boost::atomic_int c(20);
//store load
c.store(5, boost::memory_order_seq_cst);
std::cout << "隐式类型转换:" << c << std::endl; //隐式类型转换,相当于c.load()
std::cout << "使用load()取值:" << c.load() << std::endl;
//结果:
//隐式类型转换:5
//使用load()取值:5
对于exchange()函数,顾名思义,它原⼦化地“交换”两个值,在存值之后返回内部原有的值。
std::cout << "exchange前的值:" << c.exchange(15) << std::endl;
std::cout << "exchange后的值:" << c << std::endl;
//exchange前的值:5
//exchange后的值:15
compare_exchange_weak()和compare_exchange_strong()是exchange()的增强版本,也就是常说的CAS(compare-and-swap)操作。它们⽐较expected,如果相等则存值为 desired,返回true/false表示原值是否被修改,但⽆论怎样,最后在expected变量⾥都会输出原 值。
两者的区别是compare_exchange_weak()的执⾏速度快,但使⽤它即使执⾏成功也可能 会返回false。
int expected = 14;
bool ischange = c.compare_exchange_weak(expected, 16);
if (ischange)
{
std::cout << "compare_exchange_weak后的值:" << c << std::endl;
}
else {
std::cout << "compare_exchange_weak failed" << std::endl;
}
std::cout << "expected:" << expected << std::endl;
//compare_exchange_weak failed
//expected:15
expected = 15;
ischange = c.compare_exchange_strong(expected, 16);
if (ischange)
{
std::cout << "compare_exchange_strong后的值:" << c << std::endl;
}
else {
std::cout << "compare_exchange_strong failed" << std::endl;
}
std::cout << "expected:" << expected << std::endl;
//compare_exchange_strong后的值:16
//expected:15
atomic的成员函数storage()可以直接获得atomic内部值的引⽤,它能够以任意⽅式操作数据, 但它也因此⽆法提供原⼦保证,在并发环境⾥我们尽量不要使⽤它。使用时需要添加宏定义:
#define BOOST_ATOMIC_SILENCE_STORAGE_DEPRECATION
整数和bool类型是程序中最常⽤的数据类型,原⼦化后的atomic<I>和atomic<bool>的⽤法和原始类型类似,可以安全地将它们⽤在并发环境⾥,它们能够被多个CPU核⼼或线程并发访问⽽ ⽆须特意使⽤保护⼿段,从⽽担当计数器或标志位的⻆⾊。
atomic<I>是⼀类特殊的atomic对象,它除了具有基本的原⼦操作,还有整数特有的⼀些 fetch_xxx操作,它可以执⾏对应的数学运算,然后返回原值⽽不是运算后的值。atomic<I>也 重载了“++”“+=”等操作符,这些操作符重载函数内部调⽤了fetch_xxx函数,但返回运算后的值。
boost::atomic<int> d(100);
int value = d.fetch_add(1);
std::cout <<"原值:"<< value << std::endl;
std::cout <<"fetch_add后的值:"<< d << std::endl;
value = ++d;
std::cout <<"前置++后的值:"<< value << std::endl;
//原值:100
//fetch_add后的值:101
//前置++后的值:102
atomic<bool>⽐atomic<I>更特殊⼀些,虽然它也属于整数,但它只有true/false两个取值,所以它没有fetch_xxx操作,也没有操作符重载,其接⼝与基本的atomic<T>相同(但内部实现不同)。
boost::atomic<bool> _bool(false);
auto x = _bool.exchange(true);
std::cout <<"原值:"<< x << std::endl;
std::cout <<"exchange后的值:"<< _bool << std::endl;
//原值:0
//exchange后的值:1
(可参考文章:C++ 原子操作(6种原子顺序)_小乌龟的博客-CSDN博客_c++ 原子操作)
在现代多CPU核⼼并发的环境⾥,编译器和CPU的优化都有可能打乱指令的执⾏顺序,虽然这可能会获得更⾼的执⾏效率,但也可能产⽣副作⽤,导致程序的流程不⼀定按照代码的顺序执⾏。
atomic库在头⽂件<boost/memory_order.hpp>⾥定义了内存访问顺序的概念,它是⼀个简单的枚举类型,允许⽤户⾃⼰控制代码顺序的⼀致性。
atomic<T>的每个成员函数都有⼀个memory_order默认参数(操作符重载函数除 外),它指定了原⼦操作的内存访问顺序要求。 memory_order参数的默认值是memory_order_seq_cst,它是最严格的顺序⼀致性的约束,不允许编译器或CPU核⼼为优化⽽调整代码或指令的执⾏顺序,保证在并发环境⾥任何CPU核⼼“看到”的指令顺序都是相同的,其程序的执⾏与单CPU单线程时相同,简化了程序员的⼯作。
enum class memory_order : unsigned int
{
relaxed = 0,
consume = 1,
acquire = 2,
release = 4,
acq_rel = 6, // acquire | release
seq_cst = 14 // acq_rel | 8
};