C++之路(2)atomic
介绍<atomic>标准库的原子量与操作。

C++之路(2)atomic

从C++11标准开始,C++开始支持原子量与原子操作。C++20增加 wait 功能。

#include <atomic>

一、原子量

  • std::atomic_bool 即为 std::atomic<bool>
  • std::atomic_int 即为 std::atomic<int>
  • std::atomic_uint 即为 std::atomic<unsigned int>
  • std::atomic_flag,不同于atomic<>,它保证是lock-free的。比std::atomic<bool>速度快,不同于std::atomic<bool>,它不提供loadstore操作;而是提供 testtest_and_setclear 操作。

C++20另支持 std::atomic<std::shared_ptr>std::atomic<std::weak_ptr>

注意: 所有 std::atomic 都不支持 copyablemovable

二、原子量的操作

  • store 赋值
  • load 取值
  • exchange 赋新值并取原值
  • fetch_add 加上
  • fetch_sub 减去
  • compare_exchange_weak/compare_exchange_strong 与第一个参数比较:如果相等,用第二个参数赋新值,返回 true ;如果不相等,取值并赋给第一个参数,返回 false
    • weak 版本可能无故地fail(fail spuriously),但速度快!使用时外层需要有循环(while)保护,防止无故fail。
    • strong 版本:如果仅仅为了防止无故fail而将weak版本放入循环,则不如直接使用strong版本。

三、原子量操作的重要参数:memory_order

1. 理解 memory order 的背景知识

代码书写顺序不代表运行顺序。书写一句代码编译后往往被分解成多句汇编及机器语言。
编译器优化经常把不相干的语句调整顺序;CPU运行优化时也经常将不相干的语句并行运行。但优化绝对不改变对相同变量的关联性操作的顺序逻辑,因此在单线程中,这些优化都不会造成任何问题。感觉上,运行顺序与书写顺序一致。

而在多线程编程中,编译器对语句顺序的优化调整和CPU内部的并行运算,相互交叉,将会造成超出每个线程书写顺序理解的诡异结果。
最简单的解决多线程上述问题的方法是用 mutex 等的锁;而想追求极致速度,可使用无锁编程技术。无锁编程的基础就是深刻理解运用 memory order

memory order 是约束单一线程中原子操作语句前后不相干语句的执行顺序能够以什么程度被重新排列优化。

2. memory order 参数

typedef enum memory_order {
    memory_order_relaxed,
    memory_order_consume,
    memory_order_acquire,
    memory_order_release,
    memory_order_acq_rel,
    memory_order_seq_cst
} memory_order;
  1. memory_order_relaxed :最宽松(无)约束。只保证本操作的原子性,对其他读写操作没有同步和顺序限制。(遵从最原始的优化原则,同一线程对变量的相关操作被约束顺序,不相关操作可能被无约束改变顺序进行优化)
  2. memory_order_consume :取值操作,保证这句在最前,即当前线程没有其他相关读写操作被优化到本次读值之前。(目前C++标准不鼓励使用)
  3. memory_order_acquire :取值操作,保证这句在最前,即当前线程没有其他任何读写操作被优化到本次读值之前。(例如:mutex的lock操作)
  4. memory_order_release :赋值操作,保证这句在最后,即当前线程没有其他任何读写操作被优化到本次赋值之后。(例如:mutex的unlock操作)
  5. memory_order_acq_rel :取值并改写的操作,相当于acquirerelease
  6. memory_order_seq_cst :取值操作,相当于acquire;赋值操作,相当于release;取值并改写的操作,相当于acq_rel;并且附加更严格的控制顺序,保证所有线程所有修改按照同样的顺序。(C++的默认)

注意: C++原子操作默认 order 的参数都用 memory_order_seq_cst 即满足 sequentially consistent ordering,牺牲了性能。

3. 多线程中 memory order 的使用

  1. Sequentially-consistent ordering :赋值、取值用 seq_cst 。最强的顺序约束。牺牲性能最大。(C++的默认)
  2. Release-Acquire ordering :一个线程赋值用release;另一个线程取值用acquire。顺序强约束。(x86架构机器绝大多数的操作支持这个,不支持更松的约束了)
  3. Release-Consume ordering :一个线程赋值用release;另一个线程取值用consume。很少修改但经常读取的数据结构可以用这个。(目前C++标准不鼓励用)
  4. Relaxed ordering :赋值、取值用relaxed。最松约束、最优性能。(arm架构机器支持,x86架构机器不支持)

4. 总结

C++原子操作的默认 Sequentially-consistent ordering 约束太强,牺牲性能。
建议显性使用 Release-Acquire ordering ,即 storereleaseloadacquire

四、原子量的 wait/notify

C++20后,原子量开始支持 waitnotify_onenotify_all

通常比自旋锁spinlock效率高。

void wait( T old, std::memory_order order = 
                    std::memory_order::seq_cst);
void notify_one();
  1. 调用时,判断等于old,进入wait;不等于old,不wait,直接return。
  2. notify_onenotify_all唤醒,也可能被无故唤醒(spuriously)。
  3. 被唤醒时,仅当不等于old时,才return;如相等,继续wait。
  4. 调用时建议使用 std::memory_order::acquire

最后修改于 2024-02-24