美文网首页
C++并发编程 - 原子操作

C++并发编程 - 原子操作

作者: 开源519 | 来源:发表于2022-09-13 18:05 被阅读0次

所谓原子操作是指不会被线程调度机制打断的操作;这种操作一旦开始,就一直运行到结束,中间不会有任何 context switch(切换到另一个线程)。 --百度百科

  原子操作可以保证正在进行的动作不被打断,即一旦开始,持续结束。对比互斥锁其优势在于,原子操作在C/C++的层面,是无锁操作,其既能解决并发问题又不会导致死锁。

原理

  先说明一下,非原子操作。从开始执行到结束的过程中,可能会被其他任务打断的操作,就称为非原子操作。假如,多个任务操作的不是同一块内存,不会存在问题;如若操作了同一块内存,就可能引起很严重且难以排查的bug。

  在X86平台,CPU提供了在指令执行期间对总线加锁的手段。CPU上有一根引线#HLOCK pin连到北桥,如果汇编语言的程序中在一条指令前面加上前缀“LOCK”,经过汇编以后的机器代码就使CPU在执行这条指令的时候把#HLOCK pin的电位拉低,持续到这条指令结束时放开,从而把总线锁住,这样同一总线上别的CPU就暂时不能通过总线访问内存了,保证了这条指令在多处理器环境中的原子性。

使用场景

  在多线程的代码中,同时操作一个普通的变量,经过测试,会存在某些严重的bug。

void value_add_test()
{
  int shareValue = 0;

  auto f = [&](const char *name, int idx) {
      for (int i = 0; i < 10000; i++) {
          ++shareValue;
          usleep(5);          // 使多个线程相互切换
          LOG("%s%d: shareValue %d\n", name, idx, shareValue);
      }
  };

  for (int i = 0; i < 100; i++)
  {
      aThreads[i] = std::thread(f, "thread", i);
  }

  for (int j = 0; j < 100; j++)
  {
      aThreads[j].join();
  }

  LOG("shareValue: %d\n", shareValue);
}

执行结果

... // 省略
thread96: shareValue 999839
thread96: shareValue 999840
thread96: shareValue 999841
thread96: shareValue 999842
shareValue: 999842

  预期执行完毕i的值为1000000,但在Ubuntu20.04.1的版本上执行的结果为999842(计算结果不固定)。

  分析原因:++shareValue在汇编中分为三步:读取数据; shareValue加1;将shareValue值写入内存。可能会存在某个线程在进行第二步的同时,其他线程执行第三步或者第一步就造成了值的混乱。

解决方法
可通过互斥锁或者原子操作解决。相对于互斥锁,原子操作的使用更为方便,只需要将操作的变量声明为原子操作即可。

void value_add_test()
{
  std::atomic<int> atomicValue(0);

  auto f = [&](const char *name, int idx) {
      for (int i = 0; i < 10000; i++) {
          ++atomicValue;
          usleep(5);          // 使多个线程相互切换
          LOG("%s%d: atomicValue %d\n", name, idx, atomicValue.load());
      }
  };

  for (int i = 0; i < 100; i++)
  {
      aThreads[i] = std::thread(f, "thread", i);
  }

  for (int j = 0; j < 100; j++)
  {
      aThreads[j].join();
  }

  LOG("atomicValue: %d\n", atomicValue.load());
}

执行结果

... //省略
thread84: atomicValue 999996
thread84: atomicValue 999997
thread84: atomicValue 999998
thread84: atomicValue 999999
thread84: atomicValue 1000000
atomicValue: 1000000

  把普通变量用原子变量替换后,其值就正确了。本例使用的std::atomic<int>,其支持++、--操作。其他类型的原子变量可能不支持此操作。

std::atomic:指针运算

  原子指针类型,可以使用内置类型或自定义类型T, 通过特化 std::atomic<T*> 进行定义。其使用方法与标准的原子整形使用方式类似。

std::atomic<T*>为指针运算提供新的操作。 基本操作有fetch_add()fetch_sub()提供,它们在存储地址上做原子加法和减法, 为+=, -=, ++和--提供简易的封装。

std::atomic<>主要类的模板

  针对常用的类型,C++11都有对应的原子类型,不同的原子类型开放的接口有些许差异,如下表:


原子操作.png

总结

  • 常规的原子操作与普通变量类型使用起来相差不大,其保证变量在修改时不被打断,避免并发导致的一些异常问题。

  • 原子操作不存在死锁问题,因此在并发编程中,"临界区"较简单都可以使用原子操作代替互斥锁。

  • 本文仅包含原子操作的介绍及简单使用,《C++并发编程实战》中对原子操作的描述有很大一部分在本文未体现。后续若涉及到原子操作其他方面的使用,再做补充。

参考

https://blog.csdn.net/yuntongsf/article/details/9197813
https://forsworns.github.io/zh/blogs/20210822/
《C++并发编程实战》

相关文章

  • 2018-11-11 #C++ 内存模型#

    C++ 原子操作内存模型 C++ 原子操作内存模型解决并发编程的什么问题? 相信在大多数应用程序员眼里,代码在编辑...

  • C++并发编程 - 原子操作

    所谓原子操作是指不会被线程调度机制打断的操作;这种操作一旦开始,就一直运行到结束,中间不会有任何 context ...

  • Go语言——原子操作

    Go语言——原子操作 参考: 《Go并发编程实战(第2版)》 Background 原子操作即执行过程不能被中断的...

  • 从CAS讲起,真正高性能解决并发编程的原子操作

    今天呢!灯塔君跟大家讲: 高性能解决并发编程的原子操作 一.原子性操作 原子性操作:原子性在一个操作是不可中断的,...

  • 2020年Java并发试题整理

    2019年Java并发试题整理(答案) 1、并发编程三要素? (1)原子性 原子性指的是一个或者多个操作,要么全部...

  • J.U.C-atomic包

    原子操作类介绍 atomic包下为原子操作类。在并发编程中很容易出现并发安全的问题,有一个很简单的例子就是多线程更...

  • 面试官:说说 Java CAS 原理?

    在并发编程中我们都知道i++操作是非线程安全的,这是因为 i++操作不是原子操作。 如何保证原子性呢?常用的方法就...

  • Java并发——volatile、synchronized、lo

    在并发编程中有三个典型问题:原子性问题,可见性问题,有序性问题。 原子性问题 原子性:即一个操作或者多个操作 要么...

  • 第二章 并发机制的底层实现原理

    在JVM和CPU层面分析Java如何实现并发编程。 volatile、synchronized和原子操作的实现原理...

  • Java并发编程(7):使用synchronized获取互斥锁的

    在并发编程中,多线程同时并发访问的资源叫做临界资源,当多个线程同时访问对象并要求操作相同资源时,分割了原子操作就有...

网友评论

      本文标题:C++并发编程 - 原子操作

      本文链接:https://www.haomeiwen.com/subject/yoyhortx.html