在当多个线程对一个共享数据同时更新时,这个可能会导致数据竞争,即对一个线程对该数据的一次操作在汇编上是多个步骤的,所以当多个现场同时进行操作时,这些步骤就可能会穿插在一起,导致数据修改错误:
- #include
- #include
- #include
- #include
- #include
- using namespace std;
-
- int share_data;
-
- void f() {
- for (int i = 0; i < 1000000; i++) {
- share_data++;
- }
- }
-
- int main() {
- vector
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
atomic类封装了T,但是因为局限性,只能实现操作重载操作符操作和store和load操作;
下面是atomic
atomic
下面是这些操作的介绍【在其他网站上抄的C++ 标准库 -
- 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
::value; - ret = std::is_copy_constructible
::value; - ret = std::is_move_constructible
::value; - ret = std::is_copy_assignable
::value; - ret = std::is_move_assignable
::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 :完全按照语句顺序进行
要注意,对内存顺序要求越高的内存顺序重排方式对性能影响越大;
要好好理解我上面用树结构比喻的内存顺序;