c++11多线程编程 整理(五) 原子量 atomic

https://baptiste-wicht.com/categories/c%2B%2B11-concurrency-tutorial.html

https://baptiste-wicht.com/posts/2012/07/c11-concurrency-tutorial-part-4-atomic-type.html

上面可以先看一下;

概述

在多线程模式下为了保证线程安全,我们通常做法是给共享资源加互斥锁,在一段时间只能有一个线程访问并操作共享资源,其他线程都会阻塞,这样就在一些对性能要求很高的情况下可能加锁就显得有点过重,于是就想出能不能在汇编层面让系统来能保证线程不乱来,让一个线程能把它想做的事做完其他线程再来操作,这就引出原子操作的概念。

原子操作就是将一系列操作能看成一个原子,不可分割,要么每个步骤都完成,要么都不做,同时能够按照顺序进行

想想典型的i++问题,这其中包含了三个步骤,先是取i的值到寄存器中,然后是在寄存器中加1,然后又将值读回内存中。未加任何限制的多线程程序中这三个步骤就会交叉执行,可能存在线程A在寄存器加1,但还未将加1后的值又读回内存,线程B来读取此时内存的i值,也加1,这样最后的结果就是错误的。对于这种情况我们就可以想到可以把这三个步骤包装在一起组成一个原子操作。c++11引入atomic标准文件,用atomic<T>来定义原子量,就封装了一系列的原子操作,具体的见后文。

再来看另一种情况,在多线程中我们常常会使用条件变量

//线程A
int a = 1;
if(b == 0){
    do something...
    b = 1;
}

//线程B
if(b == 1){
    if(a == 1){
        ok
    }
}

 

你觉得最后能ok吗,其实是不一定的。由于编译器不同等级的优化和cpu的乱序执行,一个线程内的指令执行顺序可能会发生变化,即指令重排,在线程A中变量a和变量b是不相关的,编译器就不会注重她两谁先谁后,可能就会先进行if语句,再执行a的定义语句,但可以看到这样就会影响线程B执行的结果。试想这种情况可不可以明确告诉编译器对变量的读写操作顺序不能乱来,这个就可以通过内存序来实现,c++11引入了6种内存序,来作为原子量成员函数的参数,具体见后文

.
.

atomic

说了这些铺垫,下面来看看这个原子量具体是个什么东东。来看下它的结构体定义

template < class T > struct atomic {
    //判断atomic<T>中的T对象是否为lock_free
    bool is_lock_free() const volatile;
    bool is_lock_free() const;
    
    //基本的构造函数,重载函数
    atomic() = default;
    constexpr atomic(T val);
    atomic(const atomic &) = delete;
    atomic & operator=(const atomic &) = delete;
    atomic & operator=(const atomic &) volatile = delete;
    T operator=(T val) volatile;
    T operator=(T val);
    operator  T() const volatile;
    operator  T() const;
    
    //一系列封装的原子操作
    T exchange(T val, memory_order = memory_order_seq_cst) volatile;
    T exchange(T val, memory_order = memory_order_seq_cst);
    void store(T val, memory_order = memory_order_seq_cst) volatile;
    void store(T val, memory_order = memory_order_seq_cst);
    T load(memory_order = memory_order_seq_cst) const volatile;
    T load(memory_order = memory_order_seq_cst) const;
    bool compare_exchange_weak(T& expected, T val, memory_order = memory_order_seq_cst) volatile;
    bool compare_exchange_weak(T &, T, memory_order = memory_order_seq_cst);
    bool compare_exchange_strong(T &, T, memory_order = memory_order_seq_cst) volatile;
    bool compare_exchange_strong(T &, T, memory_order = memory_order_seq_cst);
};

是不是感觉成员很多,不慌。除去基本的构造函数,运算符重载函数,我们主要来看下面一系列的函数 

 

    //一系列封装的原子操作
    T exchange(T val, memory_order = memory_order_seq_cst) volatile;
    T exchange(T val, memory_order = memory_order_seq_cst);
    void store(T val, memory_order = memory_order_seq_cst) volatile;
    void store(T val, memory_order = memory_order_seq_cst);
    T load(memory_order = memory_order_seq_cst) const volatile;
    T load(memory_order = memory_order_seq_cst) const;
    bool compare_exchange_weak(T& expected, T val, memory_order = memory_order_seq_cst) volatile;
    bool compare_exchange_weak(T &, T, memory_order = memory_order_seq_cst);
    bool compare_exchange_strong(T &, T, memory_order = memory_order_seq_cst) volatile;
    bool compare_exchange_strong(T &, T, memory_order = memory_order_seq_cst);

几个函数主要就是实现对T的一些操作的封装,比如读写操作,比较and更改操作,其实这还只是一部分,对于不同的class T类型还有特化版本,里面还有一些不同的成员函数,但就处于先理解的目的,我们来看看是怎么用的,比如对于T是整数或指针就有对i++操作符的重载,使其自增操作成为一个原子操作,接下来我们来用用它

#include <iostream>
#include <atomic>
#include <vector>
#include <functional>
#include <thread>
using namespace std;

std::atomic<long> sum;
const int count = 100000;

void add()
{
    //每个线程来循环对sum实行加1操作
    for (int j = 0; j < count; ++j)
        sum++;
}

int main()
{
    std::vector<std::thread> threads;
    //开始计时
    clock_t start = clock();

    for (int j = 0; j < 5; ++j)
        threads.push_back(std::move(std::thread(add)));

    for (auto & t : threads)
        t.join();
    //结束计时
    clock_t finish = clock();
    cout<<"result:"<<sum<<endl;
    cout<<"time:"<<finish -start<<"ms"<<endl;

    return 0;
}

可以看到最后的结果正确,如果把sum换成普通的变量结果就不对


那又来试试加锁

for (int j = 0; j < count; ++j){
        m.lock();
        sum++;
        m.unlock();
    }

可以看到运行时间比用原子量还是多了一个量级。
这里再提一点,atomic当class T为某些类型时其内部原子操作封装也是会用锁的…,可以用其成员函数is_lock_free()来判断这个类型的原子量是不是无锁的
.
.

内存序

可以看到atomic成员函数里有这样一个参数memory_order

 bool compare_exchange_weak(T& expected, T val, memory_order = memory_order_seq_cst) volatile;

这个就是前文提到的内存序,这里就简单说说了

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_acquire:要求当前调用之后的读写操作不会重排到前面来

memory_order_release:要求这个调用之前的读写操作不能重排到后面去

memory_order_acquire,memory_order_release一般配合使用,即得到获取释放一致性,就拿前面的条件变量的例子来继续说,将其中的变量b换成原子量

atomic<int> b;

void A()
{
    int a = 0;
    b.store(1,memory_order_release);
}


void B()
{
   if(b.load(memory_order_acquire) == 1)
        cout<<"ok"<<endl;
   if(a == 0){}
}

A中release就保证int a = 0 不会调到b.store后面运行
B中acquire就保证if(a == 0 )不会调到b.load的前面

这是最常用的一种无锁搭配。

memory_order_acq_rel:读取-修改-写回
memory_order_seq_cst:默认的内存序
memory_order_consume:防止在其后对原子变量有依赖的操作被重排到前面去
memory_order_relaxed:不对执行顺序做保证,编译器和处理器可以随意优化

其他四个都不怎么用,而且很容易出错。

.
.

总结

到此感觉也把我知道的写得差不多了,当下最普遍的还是加锁来保证线程安全,但很多时候加锁会显得很繁重,原子量就是从更底层的层面来保证线程安全,所以效率更高

 

其实看到 这里,学过 JAVA的同学;语言同源,基本是一样的;

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

恋恋西风

up up up

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

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

打赏作者

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

抵扣说明:

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

余额充值