C++多线程之旅-atomic原子类型

前言

多线程的一个重要问题就是并发,但是这个并发不是简单的一个线程访问A段内存,一个线程访问B段线程,这样显然不会出现任何问题。但是一旦两个线程同时访问同一段内存就会出现未知的错误。在前面讲过了使用互斥元的方法,可以保证只有一个线程能访问易发生冲突的内存区域。另外一种方法就是使用原子变量同步。

atomic

原子类型在头文件<atomic>中,使用atomic有两套命名模式:一种是使用替代名称,一种是使用atomic的特化。

原子类型对应特化
atomic_boolatomic< bool >
atomic_charatomic< char >
atomic_intatomic< int >

在同一个程序中不要混用两种方式,容易出现代码不可移植。

通常atomic仅限一下这几种操作:load()store()exchange()compare_exchange_weak()compare_exchange_strong()

但是对于其中特殊的atomic_flag必须使用ATOMIC_FLAG_INIT进行初始化,只能有三类操作,分别是销毁、清除和设置并查询,对应的就是析构函数、clear()函数以及test_and_set()函数。并且atomic_flag不允许拷贝构造和拷贝赋值。

下面看一个例子:

class my_mutex {
    atomic_flag flag;
public:
    my_mutex():flag(ATOMIC_FLAG_INIT){}
    void lock(){
        while (flag.test_and_set(std::memory_order_acquire));
    }
    void unlock(){
        flag.clear(memory_order_release);
    }
};

my_mutex mx;

void fun() {
    mx.lock();
    cout << "hello" << endl;
    mx.unlock();
}

这里使用了clear清除之前在里面存储的值,而test_and_set()则是一直查询里面的值,然后返回,如果返回的是true则继续循环,直到为false时才重新设置。

通过原子变量来实习互斥量的功能,但是这里阻塞是用的循环,直到循环条件不满足时才退出。里面的memory_order_ 在下一讲来慢慢展开。

除了atomic_flag之外其他的类都拥有其他的成员函数,下面就来依次介绍。

成员函数

load()

加载原子对象中存入的值,等价与直接使用原子变量。

atomic<bool> b;
bool x = b.load(memory_order_acquire);

store()

存储一个值到原子对象,等价于使用等号。

b.store(true);

看一下下面这两个例子:

void fun_2(){
    atomic<int> b;
    b.store(10);
    cout << b.load() << endl;
    b = 50;
    cout << b << endl;
}

而且还可以添加内存模型参数,下面就是执行结果:
在这里插入图片描述

exchange()

返回原来里面存储的值,然后在存储一个新的值,相当于将上面两个load()store()合成起来的参数。

void fun_2(){
    atomic<int> b;
    b.store(10);
    cout << b.exchange(100) << endl;
    cout << b;
}

等价于

void fun_3(){
	cout << b.load();
	b.store(100);

但是exchange()是作为一个原子操作,而下面两个单独的组合却是两个的原子操作的组合,不再是原子操作。

compare_exchange_weak()

交换-比较操作是比较原子变量值和所提供的期望值,如果二者相等,则存储提供的期望值,如果不等则将期望值更新为原子变量的实际值,更新成功则返回true反之则返回false

atomic<bool> b;
b.compare_exchange_weak(expected, new_value);

当存储的值和expected相等时则将则更新为new_value,如果不等时则不变。其中expected必须是类型变量,而不能是常量。

void fun_3(){
    atomic<int> b;
    b = 20;
    int a  = 20;
    bool x = b.compare_exchange_weak(a, 30);
    cout << "第一次交换成功?" << x << "   b = " << b << endl;
    a  = 300;
    x = b.compare_exchange_weak(a, 30);
    cout << "第二次交换成功?" << x << "   b = " << b << endl;
}

在这里插入图片描述
测试结果也印证了前面的介绍。

compare_exchange_weak可能出现即使原始值和期望值相等时,也有可能出现存储不成功,这种情况下变量的值将不会改变,并且返回false 这就是 伪失败

compare_exchange_strong()

不像compare_exchange_weak,此版本必须始终true在预期确实与所包含的对象相等时返回,不允许出现虚假故障。但是,在某些计算机上,对于某些在循环中进行检查的算法,compare_exchange_weak 可能会明显改善性能。

其余使用方法和compare_exchange_weak完全一致。

其他

针对std::atomic<T*>还有特殊操作,就是指针的移位操作。基本操作由fetch_add()fetch_sub()提供,可以提供在存储地址上原子加减法操作。

还有就是fetch_or()fetch_and()fetch_xor(),以及对应的+=,-=,|=,&=,^=,++,–。都是可以实现的。

总结

这是使用成员函数的方法,还有一种是使用自由函数,即自由函数(&原子变量,相关参数);,这是为了和C语音兼容,常见命名规则就是在原有成员函数的基础上加上一个atomic_头即可。

可用的原子操作有载入(load)、存储(store)、交换(exchange)和比较/交换(compare & exchange),而且支持第一个参数使用shared_ptr<>


版权声明:本文为deng821776892原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接和本声明。