std::atomic<>

在当多个线程对一个共享数据同时更新时,这个可能会导致数据竞争,即对一个线程对该数据的一次操作在汇编上是多个步骤的,所以当多个现场同时进行操作时,这些步骤就可能会穿插在一起,导致数据修改错误:

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

int share_data;

void f() {
	for (int i = 0; i < 1000000; i++) {
		share_data++;
	}
}

int main() {
	vector<thread> ths;
	for (int i = 0; i < 10; i++) {
		ths.push_back(thread(f));
	}
	for (int i = 0; i < ths.size(); i++) {
		ths[i].join();
	}
	cout << share_data;
}

这个输出结果并不是1e7,而是一个随机值,其原因上面已经描述了;

当然,为了保证这个工程的准确性,我可以用一个锁来完成,但每次要显示的调用锁,这很麻烦,也不是我们希望做的,我希望有一个将锁与临界区这些晦涩的概念与我隔离的抽象,c++有所回应;

在c++在标准库中添加了atomic库,里面提供了一些工具可以做到我所说的期望;

里面完成这个任务的类是atomic<T> 模板类,当然,因为是模板类,所以肯定是不能做到无锁并发的【因为T可能是一个很复杂的类,我们不可能研发出一套广泛适用于所有类的逻辑去实现无锁并发,所以只有atomic_flag这个单纯的bool封装能够做到无锁并发,而atomic<bool>虽然也是bool的封装,但是其与T共享实现逻辑,所以不保证无锁并发】;

atomic类封装了T,但是因为局限性,只能实现操作重载操作符操作和store和load操作;

下面是atomic<int>支持的操作:

 atomic<vector<int>>支持的操作:

 下面是这些操作的介绍【在其他网站上抄的C++ 标准库 - <atomic> (w3schools.cn)

1	atomic_is_lock_free
它用于检查原子类型的操作是否是无锁的

2	atomic_store & atomic_store_explicit
它自动用非原子参数替换原子对象的值

3	atomic_load & atomic_load_explicit
它以原子方式获取存储在原子对象中的值

4	atomic_exchange & atomic_exchange_explicit
它用非原子参数原子地替换原子对象的值并返回原子的旧值

5	atomic_compare_exchange_weak & atomic_compare_exchange_weak_explicit & atomic_compare_exchange_strong & atomic_compare_exchange_strong_explicit
它以原子方式将原子对象的值与非原子参数进行比较,如果相等则执行原子交换,否则执行原子负载

6	atomic_fetch_add & atomic_fetch_add_explicit
它将非原子值添加到原子对象并获得原子的先前值

7	atomic_fetch_sub & atomic_fetch_sub_explicit
它从一个原子对象中减去一个非原子值并获得原子的先前值

8	atomic_fetch_and & atomic_fetch_and_explicit
它用非原子参数的逻辑与结果替换原子对象,并获得原子对象的先前值

9	atomic_fetch_or & atomic_fetch_or_explicit
它用非原子参数的逻辑或结果替换原子对象,并获得原子对象的先前值

10	atomic_fetch_xor & atomic_fetch_xor_explicit
它用非原子参数的逻辑异或结果替换原子对象,并获得原子的先前值

--------------------------------------------------------------------------------------

1	atomic_flag
无锁布尔原子类型

2	atomic_flag_test_and_set & atomic_flag_test_and_set_explicit
它以原子方式将标志设置为 true 并返回其先前的值

3	atomic_flag_clear & atomic_flag_clear_explicit
它以原子方式将标志的值设置为 false

 atomic对象保证每次操作都是原子级别的;

但天下没有免费的午餐,用atomic也对T有很强的要求:

    auto ret = std::is_trivially_copyable<T>::value;
    ret = std::is_copy_constructible<T>::value;
    ret = std::is_move_constructible<T>::value;
    ret = std::is_copy_assignable<T>::value;
    ret = std::is_move_assignable<T>::value;

我们得保证上面的断言都为true能用atomic封装T;

内存顺序:

 本菜看了《conccurrency in action》这本书后将这篇文章当作笔记了,以及在网上找了各种文章后写这篇文章作为总结,所以这篇文章并不是讲解性质的;

首先我看见这本书刚介绍atomic的时候我是震惊的,what?!语句的顺序还能与写代码时规定的顺序不相同?

a=1;
b=2;

这可能b=2后才a=1???

后来想想也对,这两句没有相互依赖的关系,我们可以把整个语句执行顺序看成一颗树,每行代码是一个节点,两行代码之间没有相互依赖关系的话,在单线程的时候是没有关系的,因为是一行一行语句执行的,只要我们把每一行语句所依赖的语句在执行之前运行完就行;

但是这种情况在多线程的时候就不适用了,因为可能一个线程的运行准确性依赖与另外一个线程的执行顺序,这很容易做到;

所以这边就得依靠内存顺序这个概念来解决这个问题了,这个概念所衍生出来的工具的作用就是来按照我们程序员的意愿来规定单独一个线程中代码行的执行顺序。

当然,这个顺序的编排方式是有选择的,比如relaxed、release、acquire等等,这些枚举类型在std::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;

当我们对atomic进行操作的,我们可以给方法传入第二参数即std::memory_order中来指定语句的内存顺序,即对线程中的语句进行重排;

下面对这些枚举类型的重排意义进行解释:

std::memory_order::	memory_order_relaxed :无所谓顺序,只保证本操作的原子性
std::memory_order::	memory_order_acquire :保证在本线程中,在后续的读操作必须在本语句执行后在进行
std::memory_order:: memory_order_release :保证在本线程中,前置的写操作必须在本语句执行前完成执行
std::memory_order:: memory_order_acq_rel :同时拥有acquire和release的意义
std::memory_order:: memory_order_consume :保证与本原子对象相关的后续操作必须在本语句执行完成后再执行
std::memory_order:: memort_order_seq_cst :完全按照语句顺序进行

要注意,对内存顺序要求越高的内存顺序重排方式对性能影响越大;

要好好理解我上面用树结构比喻的内存顺序;

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值