煤矸石空心砖

新闻分类

联系我们Contact

企业名称:桐城市南口新型建材有限公司

联系人:崔经理

电话:0556-6568069

手机:18156911555

邮箱:303927413@qq.com

地址:桐城市龙腾街道高桥村

网址:   www.nkxxjc.com 



您的当前位置: 首 页 > 线程可以获得对任何对象的互斥锁定 > 【并发编程十三】c++原子操作(1)

【并发编程十三】c++原子操作(1)

发布日期:2023-01-30 13:29 作者: 点击:

【并发编程十三】c++原子操作(1) 一、改动序列1、改动序列2、预测执行 二、原子操作及其类别1、原子操作2、非原子操作3、原子类型 三、标准原子类型1、标准原子类型的两种实现方式2、原子操作的用途3、原子操作的宏 四、操作std:atomic_flag1、简介2、使用说明3、使用std:atomic_flag实现自旋锁3、缺点 五、操作std:atomic\1、简介2、操作3、“比较-交换”操作:依据原子对象当前的值决定是否保存新值。3.1、比较-交换3.2、compare_exchange_weak()3.3、compare_exchange_strong()3.4、compare_exchange_weak()和compare_exchange_strong()使用场景对比 五、操作std:atomic\1、简介2、原子操作、原子运算3、demo 六、标准整数原子类型七、泛化的std::\类模板八、原子操作的内存次序九、c++原子操作实现自旋锁

一、改动序列 1、改动序列 在一个c++程序中,每个对象都有一个改动序列(可以参考原子操作的内存理解std::memory_order)。它由所有线程在对象上的全部写操作构成,其中第一个写操作即为对象的初始化。大部分情况下,这个序列会随程序的多次运行而发生变化,但是在程序的任意一次运行过程中,所含的全部线程都必须形成相同的改动序列。若多个线程共同操作某一对象,但它却不属于原子类型,我们就要自己负责充分实行同步操作,进而确保对于同一个变量,所有线程就其达成一致的改动序列。变量的值会随时间推移形成一个序列。在不同的线程上观察属于同一个变量的序列,如果所见各异,就说明出现了数据竞争和未定义行为。若我们采用了原子操作,那么编译器由责任保证必要的同步操作有效、到位。 2、预测执行

预测执行又称推测执行、投机执行,是一类底层优化技术,包括分支预测、数值预测、预读内存和预读文件等,目的是在多级流水cpu上提高指令的并发度。做法是提前执行指令而不考虑师傅必要,若完成后发现没必要,则抛弃或修正预执行的结果

为了实现上述改动序列的保障,要求禁止某些预测执行(speculative execution),原因是该改动序列中,只要某些线程看过过某个对象,则该线程的后续读操作必须获得相对新进的值,并且,该线程就同一对象的后续写操作,必然出现在改动序列的后方。另外,如果某线程先向一个对象写数据,过后再读取它,那么必须读取前面写的值。若在改动序列中,上述读写操作之间还有别的写操作,则必须读取最后写的值。在程序内部,对于同一对象,全部线程都必须就其形成相同的改动序列,并且在多有对象上都要求如此,而多个对象上的改动序列知识相对关系,线程之间不必达成一致。 二、原子操作及其类别 1、原子操作 原子操作是不可分割的操作(indivisible operation)。在系统的任一线程内,我们都不会观察到这种操作处于半完成状态;它或者完全好,或者完全没做。考虑读取某对象的过程,假如其内存加载行为属于原子操作,并且该对象的全部修改行为也都是原子操作,那么通过内存加载行为就可以得到该对象的初始值,或得到某次修改而完整存入的值。 2、非原子操作 与之相反,非原子操作(non-atomic operation)在完成到一半的时候,有可能为另一线程所见。假定由原子操作组合出非原子操作,例如向结构体的原子数据成员赋值,那么别的线程有可能观察到其中的某些原子操作已完成,而某些还没开始,若多个线程同时赋值,而底层操作相交执行,本来意图完整存入的数据就会彼此错杂。因此,我们有可能观察到,也有可能得出一个“混合体"的结构体。在任何情况下访问非原子变量却欠缺同步保护,会照成简单的条件竞争,进而引发问题。具体而言,这种级别的访问可能构成数据竞争,并导致未定义行为。 3、原子类型

在c++环境中,多数情况下,我们需要通过原子类型实现原子操作。

三、标准原子类型

标准原子类型的别名,和他们对应的std::atomic特化。

类型别名定义std::atomic_boolstd::atomicstd::atomic_charstd::atomicstd::atomic_scharstd::atomicstd::atomic_ucharstd::atomicstd::atomic_shortstd::atomicstd::atomic_ushortstd::atomicstd::atomic_intstd::atomicstd::atomic_uintstd::atomicstd::atomic_longstd::atomicstd::atomic_ulongstd::atomicstd::atomic_llongstd::atomicstd::atomic_ullongstd::atomicstd::atomic_char8_t (C++20)std::atomicstd::atomic_char16_tstd::atomicstd::atomic_char32_tstd::atomicstd::atomic_wchar_tstd::atomic

还有一些其他的类型别名,可以参见std::atomic

1、标准原子类型的两种实现方式 标准原子类型的定义位于头文件内。这些类型的操作全是原子化的,并且,根据语言的定义,c++内建的原子操作也仅仅支持这些类型。尽管通过采用互斥,我们能够令其他操作实现原子化。他们全部具备成员函数is_lock_free(),准许使用者判定某一给定的类型上的操作是由原子指令(atomic instruction)直接实现(x.is_lock_free()返回true),还是要借助编译器和程序库的内部锁来实现(x.is_lock_free()返回false)。

原子指令用于在多个CPU之间维护同步关系。在一些科学计算问题中,通过并行算法把子问题分配到多个cpu上执行,但是各个子问题之间存在合作关系,因此需要硬件机制来实现多个cpu之间同步。 在这里插入图片描述 原子指令可以实现一个CPU独占执行时间。使用原子指令把连续多条指令包含起来,计算机保证只有一个cpu处于执行状态,其他cpu必须等待原子指令结束才能继续执行。(b)展示的就是实现“原子加1”的正确方法。 原子指令的实现机制一般是在cpu的互联网络中实现一个全局的原子寄存器,所有cpu对这个原子寄存器的访问是互斥的。cpu使用原子指令申请访问原子寄存器时,互联网络会对所有CPU进行仲裁,确保只有一个cpu可以获得对原子寄存器的访问权;如果有cpu获得了原子寄存器访问权,其他cpu必须等待该cpu释放权限才能继续执行。 原子指令详细介绍参见什么是原子指令

#include #include using namespace std; int main() { atomic x; bool flag = x.is_lock_free(); cout public: spinlock_mutex() { } //spinlock_mutex(const spinlock_mutex& origin); // add this line ~spinlock_mutex() {}; void lock() { while (flag.test_and_set(memory_order_acquire)); } void unlock() { flag.clear(memory_order_release); } private: atomic_flag flag = ATOMIC_FLAG_INIT; }; 3、缺点

由于atomic_flag严格受限,甚至不支持单纯的无修改查值操作,无法用作普通的布尔标志,因此最好还是使用atomic

五、操作std:atomic 1、简介 std:atomic是基于整数类型的最基本的原子类型。相比std:atomic_flag,它是一个功能更加齐全的布尔标志。尽管std:atomic也无法拷贝构造和拷贝赋值,但是我们还是能依据非原子布尔量创建其对象,初始值是true或false皆可。(该类型的实例还能接收非原子布尔量的赋值) std::atomic b(true); b=false; 2、操作 store()是写操作,true和false皆可;(相当于std:atomic_flag的成员函数clear())load()是读取操作;exchange()是”读-改-写“,(替代test_and_set()) 原子地以 desr 的值替换 obj 所指向的值,并返回 obj 先前保有的值,如同以 obj->exchange(desr) 。原子地以 desr 的值替换 obj 所指向的值,并返回 obj 先前保有的值,如同以 obj->exchange(desr, order) 。 参数 obj - 指向要修改的原子对象的指针 desr - 要存储于原子对象的值 order - 此操作所用的内存同步顺序:容许所有值。 #include #include using namespace std; int main() { atomic b; bool x = b.load(std::memory_order_acquire); b.store(true); x = b.exchange(false, std::memory_order_acq_rel); cout cout foo foo_array[5]; // 可以和boo类型类比,定义一个foo*指针,初始值是数组的第一个对象。 atomic p(foo_array); foo* x = p.fetch_add(2); // 令p+2,返回旧址。 assert(x==foo_array); assert(p.load() == &foo_array[2]); x = (p -= 1); //令p-1,返回新值 assert(x == &foo_array[1]); assert(p.load() == &foo_array[1]); return 0; } 六、标准整数原子类型

在std::atomic或std::atomic这样的整数类型上,我们可以指向的操作颇为齐全。包括:

常用原子操作:is_lock_free()、load()、store()、exchange()、compare_exchange_weak()、compare_exchange_strong();原子运算:fetch_add()、fetch_sub()、fetch_and()、fetch_or()、fetch_xor();这些运算的符合赋值形式:+=、-=、&=、|=、^=;前后缀形式的自增、自减:++x、x++、–x、x–; 七、泛化的std::类模板

主模板的存在,在除了标准原子类型之外,允许用户使用自定义类型创建一个原子变量。不 是任何自定义类型都可以使用std::atomic 的:需要满足一定的标准才行。为了使用 std::atomic (UDT是用户定义类型),这个类型必须有拷贝赋值运算符。这就意味着这个类型不能有任何虚函数或虚基类,以及必须使用编译器创建的拷贝赋值操作。不仅仅是这 些,自定义类型中所有的基类和非静态数据成员也都需要支持拷贝赋值操作。这(基本上)就允 许编译器使用memcpy(),或赋值操作的等价操作,因为它们的实现中没有用户代码。

最后,这个类型必须是“位可比的”(bitwise equality comparable)。这与对赋值的要求差不多; 你不仅需要确定,一个UDT类型对象可以使用memcpy()进行拷贝,还要确定其对象可以使用 memcmp()对位进行比较。之所以要求这么多,是为了保证“比较/交换”操作能正常的工作。

八、原子操作的内存次序

内存次序共6种,分3种模式:

先后一致次序 :memory_order_seq_cst;宽松次序 :memory_order_relaxed;获取释放次序 :memory_order_consume、memory_order_acquire、memory_order_release、memory_order_acq_rel;

库中所有原子操作的默认行为提供先后一致次序(也叫序列一致顺序)。

至于每个的区别我们在这里就不讨论了,感兴趣的可以通过文后的参考链接和书籍,自行研究。

九、c++原子操作实现自旋锁

参见【并发编程十四】c++原子操作实现自旋锁

参考: 1、https://www.apiref.com/cpp-zh/cpp/thread.html 2、《c++并发编程实战(第二版)》安东尼.威廉姆斯 著;吴天明 译;

本文网址:

关键词:线程可以获得对任何对象的互斥锁定

相关新闻: