atomic 原子操作

这是一篇关于std::atomic的文章。

std::atomic 原子操作

在我最近搞的线程池项目当中,有几个疑问。

Q:线程池的意义何在?
A:因为线程的创建与销毁,或者线程的切换开销很大。所以我们可以创建一个“随时待命”的“兵团”来避免这种开销,也就有了线程池这个项目。


Q:既然线程需要通信,或者说避免对资源的竞争,那么有没有一种方式可以开销小一点的方法呢?比锁小一点的那种。
A:有的。它就是atomic,原子操作。

传送门:

  1. atomic in cpp reference
  2. atomic in Microsoft Tutorial

作用

一个原子操作有两个关键属性,帮助你使用多个线程正确操控对象,而无需使用 mutex 锁。

  • 最直观的,“原子”指的是最小的、不可分割的。在多个线程访问同一个资源的时候,确保同一时间内只有一个线程在访问这一资源
    (就很像锁不是吗,但是原子操作更加接近底层,因而效率更高)

  • 由于原子操作是不可见的,因此,仅在第一个原子操作前后,来自不同线程同一对象上的第二个原子操作可以获取该对象的状态。

  • 基于其 memory_order 参数,原子操作可以针对同一个线程中其他原子操作的影响可见性建立排序要求。 因此,它会抑制违反排序要求的编译器优化。


可能有人有疑问了,比如对于这个例子

std::atomic_int cnt = 0;
cnt ++;

明明对于原子类型cnt的操作是原子的,为什么还要用std::atomic呢?

因为虽然看上去cnt ++是一条单独的语句,不能再被分割,但是它在汇编和编译器层面上,涉及到多个操作,比如cnt的读取、写入、自增,这些操作也可能会引起竞争。

因此,为了保证cnt的原子性,我们需要用std::atomic来包装它。

使用

原子操作是一种最小的不可分割的操作,它可以确保同一时间只有一个线程在访问某个资源。

一般用于程序计数和其他计数器,信号量,事件,条件变量等。

我在项目当中,用到的atomic基本都是对一些变量或者数据结构的原子性封装。

  • 在多线程环境中,对std::atomic对象的访问不会造成竞争冒险。利用std::atomic可实现数据结构的无锁设计。

比如

int -> std::atomic_int
bool -> std::atomic_bool

给一个使用的情景,大家可以推出atomic可以在什么时候使用

比如说,我这个线程池当中,池中内置了几个严阵以待的线程,等待着传入的任务。很自然的,我们想到使用queue队列来存储这个任务的集合,而任务的数量自然也是我们需要维护的一个变量。

但是,每一个线程都很有在同一时间对这个taskSize_进行修改操作。就很像那个cache coherence,也就是缓存一致性不是吗。

我们就可以利用对这个taskSize_加锁防止资源竞争。但是,锁又不够那么的轻量,所以进行原子封装,来确保同一时间只有一个线程在对它进行操作。

// 记录任务的数量
std::atomic_uint taskSize_ {};

std::mutex

再扯一下mutex互斥量,这个也很自然就能联想到,二者作用比较接近对吧。

一句话解决:mutex是加大范围但效率变低的atomic。

mutex可以保护的东西是一个变量,也可以是一段代码。(范围很广)