C++原子操作

C++学习资料下载:

CSDN

原子操作

        原子操作又称为原子性操作,是多线程环境中的一种同步机制。这种机制保证在操作过程中不会被线程的调度机制所打断,从而避免多个线程同时对一个数据进行操作产生的数据竞争和数据不一致的问题。

std::atomic

        C++11提供了std::atomic模板,实例化一个std::atomic对象,使得我们对一个原子类型操作由一组指令,最小化到一个CPU。

std::atomic成员函数

  • load() 读操作
  • store()写操作
  • compare_exchange_weak/compare_exchange_strong CAS操作

        compare_exchange_strong

        当前值与期望值(expect)相等时,修改当前值为设定值(desired),返回true

        当前值与期望值(expect)不等时,将期望值(expect)修改为当前值,返回false

        weak版和strong版的区别:

        weak版本的CAS允许偶然出乎意料的返回(比如在字段值和期待值一样的时候却返回了false,并且没有将字段值设置成desire的值),不过在一些循环算法中,这是可以接受的。通常它比起strong有更高的性能。

  • is_lock_free 用来检查该原子类型是否需支持原子操作

CAS:compare and set,是一条cpu原子指令。

对于整形还提供一些特殊的成员函数

  • fetch_add() 原子加
  • fetch_sub() 原子减
  • fetch_and() 原子与
  • fetch_or() 原子或

std::atomic_flag

        最简单的atomic类型,该对象可以在两个状态之间切换,设置和清除;

        对象必须被ATOMIC_FLAG_INIT初始化;

        std::atomic_flag flag = ATOMIC_FLAG_INIT;

        初始化的对象只能做三件事:销毁,清除、设置;

        对应成员函数:

        clear()
        test_and_set()

c++原子操作的六种内存顺序

atomic原子操作可以使用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;
  • memory_order_release/memory_order_acquire

memory_order_release是写操作store函数,表示该操作之前的任何写操作都不能放到该操作之后,即写语句不能调到本语句之后;

memory_order_acquire是读操作load函数,表示该读操作之后的的任何读内存操作都不放到该操作之前

对于同一个原子变量,release操作之前的写操作,一定对随后的acquire操作后的读可见,这两种内存序一般需要配对使用

  • memory_order_release/memory_order_consume

此搭配同上一个基本一样,带式该组合是一种更为宽松的内存序可见情况,comsume只是阻止对该院系变量有依赖的操作重排到前面去,而非所有读操作

  • memory_order_acq_rel

此内存序是release和acquire的结合,包含这两种的特性,因此任何读写操作的重排都不能跨越这个调用

  • memory_order_seq_cst

默认内存序选项,是最严格的内存顺序,前面的语句不能挑到后面,后面的语句不能调到前面

  • memory_order_relaxed

只保证当前语句是原子操作,对你内存顺序不做任何保证

std::atomic详解

一致性模型

        并行执行的多个线程,从某种宏观层面上讨论,可以粗略的视为一种分布式系统。在分布式系统中, 任何通信乃至本地操作都需要消耗一定时间,甚至出现不可靠的通信。 如果我们强行将一个变量 v 在多个线程之间的操作设为原子操作,即任何一个线程在操作完 v 后,其他线程均能同步感知到 v 的变化,则对于变量 v 而言,表现为顺序执行的程序,它并没有由于引入多 线程而得到任何效率上的收益。对此有什么办法能够适当的加速呢?答案便是削弱原子操作的在进程间 的同步条件。
从原理上看,每个线程可以对应为一个集群节点,而线程间的通信也几乎等价于集群节点间的通信。
削弱进程间的同步条件,通常我们会考虑四种不同的一致性模型:

1. 线性一致性:

        线性一致性又称强一致性或原子一致性。它要求任何一次读操作都能读到某个数据的最近一次写的数据,并且所有线程的操作顺序与全局时钟下的顺序是一致的。
          x.store(1)        x.load()
T1 ---------+----------------+------>
T2 -------------------+------------->
                        x.store(2)
        在这种情况下线程 T1 , T2 x 的两次写操作是原子的,且 x.store(1) 是严格的发生在 x.store(2) 之前,x.store(2) 严格的发生在 x.load() 之前。值得一提的是,线性一致性对全局时钟的要求 是难以实现的,这也是人们不断研究比这个一致性更弱条件下其他一致性的算法的原因。

2. 顺序一致性

        同样要求任何一次读操作都能读到数据最近一次写入的数据,但未要求与全局时钟的顺序一致。
       x.store(1)    x.store(3)  x.load()
T1 ---------+-----------+----------+----->
T2 ---------------+---------------------->
               x.store(2)
或者

 

           x.store(1) x.store(3) x.load()
T1 ---------+-----------+----------+----->
T2 ------+------------------------------->
        x.store(2)
        在顺序一致性的要求下,x.load() 必须读到最近一次写入的数据,因此 x.store(2) x.store(1)
并无任何先后保障,即只要 T2 x.store(2) 发生在 x.store(3) 之前即可。

3. 因果一致性

        它的要求进一步降低,只需要有因果关系的操作顺序得到保障,而非因果关系的操作顺序则不做要求。
      a = 1         b = 2
T1 ----+-----------+---------------------------->
T2 ------+--------------------+--------+-------->
        x.store(3)           c = a + b y.load()
或者
        a = 1       b = 2
T1 ----+-----------+---------------------------->
T2 ------+--------------------+--------+-------->
        x.store(3)            y.load() c = a + b
亦或者
        b = 2         a = 1
T1 ----+-----------+---------------------------->
T2 ------+--------------------+--------+-------->
        y.load()                c = a + b x.store(3)
        上面给出的三种例子都是属于因果一致的,因为整个过程中,只有 c a b 产生依赖,而 x 和 y 在此例子中表现为没有关系(但实际情况中我们需要更详细的信息才能确定 x y 确实无关)。

4. 最终一致性

       最终一致性是最弱的一致性要求,它只保障某个操作在未来的某个时间节点上会被观察到,但并 未要求被观察到的时间。因此我们甚至可以对此条件稍作加强,例如规定某个操作被观察到的时间 总是有界的。当然这已经不在我们的讨论范围之内了。
        x.store(3) x.store(4)
T1 ----+-----------+-------------------------------------------->
T2 ---------+------------+--------------------+--------+-------->
        x.read()         x.read()         x.read()         x.read()
在上面的情况中,如果我们假设 x 的初始值为 0 ,则 T2 中四次 x.read() 结果可能但不限于以下
情况:
3 4 4 4 // x 的写操作被很快观察到
0 3 3 4 // x 的写操作被观察到的时间存在一定延迟
0 0 0 4 // 最后一次读操作读到了 x 的最终值,但此前的变化并未观察到
0 0 0 0 // 在当前时间段内 x 的写操作均未被观察到,但未来某个时间点上一定能观察到 x 4 的情况。

内存顺序

        为了追求极致的性能,实现各种强度要求的一致性,C++11 为原子操作定义了六种不同的内存顺序 std::memory_order 的选项,表达了四种多线程间的同步模型。

1. 宽松模型

        在此模型下,单个线程内的原子操作都是顺序执行的,不允许指令重排,但不同线程间 原子操作的顺序是任意的。类型通过 std::memory_order_relaxed 指定。我们来看一个例子:
std::atomic<int> counter = {0};
std::vector<std::thread> vt;
for (int i = 0; i < 100; ++i) 
{
    vt.emplace_back([&](){
        counter.fetch_add(1, std::memory_order_relaxed);
        });
}
for (auto& t : vt)
{
    t.join();
}
std::cout << "current counter:" << counter << std::endl;

2. 释放/消费模型

        在此模型中,我们开始限制进程间的操作顺序,如果某个线程需要修改某个值,但 另一个线程会对该值的某次操作产生依赖,即后者依赖前者。具体而言,线程 A 完成了三次对 x 的 写操作,线程 B 仅依赖其中第三次 x 的写操作,与 x 的前两次写行为无关,则当 A 主动 x.release() 时候(即使用 std::memory_order_release ),选项 std::memory_order_consume 能够确保 B 在 调用 x.load() 时候观察到 A 中第三次对 x 的写操作。我们来看一个例子:
// 初始化为 nullptr 防止 consumer 线程从野指针进行读取
std::atomic<int*> ptr(nullptr);
int v;
std::thread producer([&]() {
    int* p = new int(42);
    v = 1024;
    ptr.store(p, std::memory_order_release);
});
std::thread consumer([&]() {
    int* p;
    while(!(p = ptr.load(std::memory_order_consume)));
        std::cout << "p: " << *p << std::endl;
        std::cout << "v: " << v << std::endl;
});
producer.join();
consumer.join();

 

3. 释放/获取模型

        在此模型下,我们可以进一步加紧对不同线程间原子操作的顺序的限制,在释放 std::memory_order_release 和获取 std::memory_order_acquire 之间规定时序,即发生在释放
操作之前的 所有 写操作,对其他线程的任何获取操作都是可见的,亦即发生顺序( happens-before )。 可以看到,std::memory_order_release 确保了它之后的写行为不会发生在释放操作之前,是一 个向后的屏障,而 std::memory_order_acquire 确保了它之前的写行为,不会发生在该获取操作 之后,是一个向前的屏障。对于选项 std::memory_order_acq_rel 而言,则结合了这两者的特点, 唯一确定了一个内存屏障,使得当前线程对内存的读写不会被重排到此操作的前后。 我们来看一个例子:
std::vector<int> v;
std::atomic<int> flag = {0};
std::thread release([&]() {
    v.push_back(42);
    flag.store(1, std::memory_order_release);
});
std::thread acqrel([&]() {
    int expected = 1; // must before compare_exchange_strong
    while(!flag.compare_exchange_strong(expected, 2, std::memory_order_acq_rel)) {
           expected = 1; // must after compare_exchange_strong
    }
// flag has changed to 2
});
std::thread acquire([&]() {
    while(flag.load(std::memory_order_acquire) < 2);
    std::cout << v.at(0) << std::endl; // must be 42
});
release.join();
acqrel.join();
acquire.join()
        在此例中我们使用了 compare_exchange_strong ,它便是比较交换原语( Compare-and-swap primitive),它有一个更弱的版本,即 compare_exchange_weak ,它允许即便交换成功,也仍然返回 false 失败。其原因是因为在某些平台上虚假故障导致的,具体而言,当 CPU 进行上下文切换时, 另一线程加载同一地址产生的不一致。除此之外,compare_exchange_strong 的性能可能稍差于 compare_exchange_weak,但大部分情况下, compare_exchange_strong 应该被有限考虑。

4. 顺序一致模型

        在此模型下,原子操作满足顺序一致性,进而可能对性能产生损耗。可显式的通过 std::memory_order_seq_cst 进行指定。最后来看一个例子:
std::atomic<int> counter = {0};
std::vector<std::thread> vt;
for (int i = 0; i < 100; ++i)
 {
    vt.emplace_back([&](){
        counter.fetch_add(1, std::memory_order_seq_cst);
    });
}
for (auto& t : vt)
{
    t.join();
}
std::cout << "current counter:" << counter << std::endl;
        这个例子与第一个宽松模型的例子本质上没有区别,仅仅只是将原子操作的内存顺序修改为了 memory_order_seq_cst,有兴趣的读者可以自行编写程序测量这两种不同内存顺序导致的性能差异。

std::atomic应用

使用std::atomic<bool> 实现一个互斥锁。

#pragma once
#include <iostream>
#include <atomic>

class MyMutex
{
public:
	MyMutex() = default;
	~MyMutex() = default;
	void lock()
	{
		bool expect = false;
		while (!flag.compare_exchange_strong(expect, true, std::memory_order::memory_order_seq_cst));
	}
	void unlock()
	{
		flag.store(false);
	}
private:
	std::atomic<bool> flag = false;
};

代码供参考,欢迎大家指正 

 

  • 7
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

瞎折腾啥啊

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值