❝所谓原子操作是指不会被线程调度机制打断的操作;这种操作一旦开始,就一直运行到结束,中间不会有任何 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>
,其支持++、--操作。其他类型的原子变量可能不支持此操作。
原子指针类型,可以使用内置类型或自定义类型T, 通过特化 std::atomic<T*> 进行定义。其使用方法与标准的原子整形使用方式类似。
std::atomic<T*>
为指针运算提供新的操作。基本操作有fetch_add()
和fetch_sub()
提供,它们在存储地址上做原子加法和减法, 为+=, -=, ++和--提供简易的封装。
针对常用的类型,C++11都有对应的原子类型,不同的原子类型开放的接口有些许差异,如下表:
https://blog.csdn.net/yuntongsf/article/details/9197813 https://forsworns.github.io/zh/blogs/20210822/ 《C++并发编程实战》
用心感悟,认真记录,写好每一篇文章,分享每一框干货。
更多文章内容包括但不限于C/C++、Linux、开发常用神器等,可进入“开源519公众号”聊天界面输入“文章目录” 或者 菜单栏选择“文章目录”查看。公众号后台聊天框输入本文标题,在线查看源码。