对于原子操作的理解

对于原子操作的理解

一、概念理解

原子操作:是一个不可分割的操作,从系统中的任何一个线程中,你都无法观察到完成了一半的这种操作,它要么做完了,要么没有做完。如果读取对象值的载入操作是原子的,并且所有对该对象的修改也都是原子的,那么这个载入操作所获得得要么是对象的初始值,要么是被修改者修改后的值。

CAS 的意思:是 Compare & Set 或者 Compare & Swap。整个过程是原子的。现代几乎所有的CPU指令都支持 CAS 的原子操作,X86 下对应的是 CMPXCHG 汇编指令。

在 c++ 的 atomic 中关于 CAS 的实现有两种:

1
2
3
4
template< class T >
bool compare_exchange_weak (T& expected, T val, memory_order sync = memory_order_seq_cst) noexcept;
template< class T >
bool compare_exchange_strong (T& expected, T val, memory_order sync = memory_order_seq_cst) noexcept;

功能:如果原子变量值和期望值相等,则将原子变量值替换为 val。如果原子变量值和期望值不相等,则将期望值替换为原子变量值。两个函数的功能一致。默认使用 memory_order_seq_cst 内存顺序。

区别:compare_exchange_wak 有可能出现虚假失败,即原子变量值和期望值相等的情况下,依然返回失败。这种情况下原子变量值是不会变化的。这种情况最有可能发生在缺少单个的比较并交换指令的机器上或者某些平台使用伪指令(不是X86上的指令),在这样的平台上,上下文切换,另一个线程重新加载相同的地址(或缓存行)等可能会使原语失败。因此 compare_exchange_wak 通常用在循环中:

1
2
3
bool expected = false;
extern atomic<bool> b;
while (!b.compare_exchange_weak(expected, true) && !expected);

在这种情况下,只要 expected 仍为 false,表明 compare_exchange_wak 调用虚假失败,应该保持循环。

而 compare_exchange_strong 没有这种虚假失败,

如果待存储的值很简单,为了避免在 compare_exchange_weak 可能会虚假失败进行循环也还好。但如果待存储的值本身是耗时的,当个 expected 值没有变化时,使用 compare_exchange_strong 来避免被迫重新计算待存储的值可能是有意义的。并且虚假失败通常很少发生,compare_exchange_weak 的性能往往比 compare_exchange_strong 要快很多。

因此,简单来说,c++ 为我们提供这两种语义,一种是“尽力而为”,一种是“我一定会做到,无论中间有多少坏事发生”。

二、c++ 中的原子操作 atomic

原子操作有三类:

  • 读:在读取的过程中,读取位置的内容不会发生任何变动。
  • 写:在写入的过程中,其他执行线程不会看到部分写入的结果。
  • 读‐修改‐写:读取内存、修改数值、然后写回内存,整个操作的过程中间不会有其他写入操作插入,其他执行线程不会看到部分写入的结果。

关于 volatile 和原子类型:Java 和 C++ 都有 volatile 关键字。但同样的关键字在不同的语言中有着不同的含义。Java 中的 volatile 和 C++ 的原子类型是类似的含义。而 C++ 中的 volatile 是禁止编译器对这个变量进行优化。

我们来看看 c++ 中为我们提供的 atomic 原子操作。c++ 中 atomic 是模版类,支持多种不同的类型,主要分为四大类,包括 atomic_flagatomic_boolatomic_指针类型atomic_整形类型。表格无差别的列出了 atomic 支持的原子操作,以及不同的类型对应对应的不同的原子操作。

操作 atomic_flag atomic_bool atomic_指针类型 atomic_整形类型 说明
test_and_set 操作 Y 将 flag 设为 true 并返回原先的值
clear 操作 Y 将 flag 设置为 false
store 操作 Y Y Y 写入对象到原子对象中
load 操作 Y Y Y 从原子对象读取到内置对象
is_lock_free 操作 Y Y Y 判断对原子对象的操作是否无锁
exchange 交换操作 Y Y Y 这是读‐修改‐写操作
compare_exchange_weak Y Y Y 比较加交换(CAS),存在虚假失败
compare_exchange_strong Y Y Y 比较加交换(CAS),不存在虚假失败
fetch_add Y Y 仅对整数和指针内置对象有效,对目标原子对象执行加操作,返回其原始值
fetch_sub Y Y 仅对整数和指针内置对象有效,对目标原子对象执行加操作,返回其原始值
++ 和 – 操作(前置和后置) Y Y 仅对整数和指针内置对象有效,对目标原子对象执行增一或减一,操作使用顺序一致性语义,并注意返回的不是原子对象的引用(这是读‐修改‐写操作)
+= 和 -= 操作 Y Y 仅对整数和指针内置对象有效,对目标原子对象执行加或减操作,返回操作之后的数值,操作使用顺序一致性语义,并注意返回的不是原子对象的引用(这是读‐修改‐写操作)
fetch_or, |= Y 求或并赋值
fetch_and, &= Y 求与并赋值
fetch_xor, ^= Y 求异或并赋值

注意:

  • 所有的原子操作都不支持拷贝和赋值。因为该操作涉及了两个原子对象:要先从另外一个原子对象上读取值,然后再写入另外一个原子对象。而对于两个不同的原子对象上单一操作不可能是原子的。
  • 没有浮点类型的原子类型
  • 没有原子操作的乘法和除法

1. atomic_flag

他是一个 bool 类型的原子类型。只有两个状态,设置(值为 true)或者清除(值为 false)。注意 atomic_flag 必须通过 ATOMIC_FLAG_INIT 初始化,将其设置为清除状态。只能这样做

1
std::atomic_flag flag = ATOMIC_FLAG_INIT;

初始化之后,atomic_flag 支持两种操作 test_and_setclear 操作。如上面的表格介绍

2. is_lock_free

通过此接口可以得知这个对象上的原子操作是否无锁。

atomic_flag 类型不支持 is_lock_free 操作,其他的原子类型都支持这个操作。C++ 规范要求 atomic_flag 的原子操作都是无锁的,其他的原子类型是否无锁就要看具体的平台实现了。

3. 读、写、交换

原子操作不支持拷贝和赋值操作,提供了如下操作来读、写、交换值。

  • load:原子的获取原子对象的值
  • store:原子的以非原子对象替换原子对象的值
  • exchange:原子的替换原子对象的值,并获取他之前持有的值

这三种操作,使用的参数和返回值都是非原子类型的。

4. 指针原子类型

对于某种类型 T 的原子类型是: atomic<T*>

指针类型除了一些正常的操作之外,还包括通过偏移值调整指针位置的操作:

  • fetch_add 和 +=:原子的增加指定的值
  • fetch_sub 和 -=:原子的减少指定的值
  • ++ 和 –:原子的自增或者自减

5. 整形原子类型

整形原子类型除了一些正常的操作之外,还包括一些位运算。支持:“或”、“与”、“异或”三种逻辑操作。

还有需要注意的是:

  • 所有命名函数:比如 fet_addfetch_or 等返回的都是修改前的数值。
  • 所有复合赋值运算符:比如:+=-=|= 等返回的是修改后的值。

三、简单总结

本小节我们对原子操作进行了分享,不过大家可能发现 atomic 的那些操作,有一个参数用来指定 memory ordering。比如:

1
T load (memory_order sync = memory_order_seq_cst) const volatile noexcept;

memory order 是一个比较重要的话题,我们留到下节重点分享。

本节内容主要分享原子操作是什么,以及 c++ 中的 atomic 原子操作的使用方式。