原子操作
在c++中标准库也提供了原子操作的模板类,在头文件#include< atomic>中
定义
计算机执行的最小单位就是指令,即CPU一次只能执行一条指令。
假定有两个操作A 和B,如果从执行A 的线程来看,当另一个线程执行B 时,要么将B 全部执行完,要么完全不执行B,那么A 和B 对彼此来说是原子的。
理想很丰满,现实很骨感.一句c语言"i++"的操作,经过编译翻译成汇编语言后成了三句汇编代码
//c语言 ==> i++
//汇编语言 ==> intel风格汇编
__asm__{
mov eax [i] //将变量i的值取出到cpu寄存器eax中
inc eax //eax自增1
mov [i] eax //将寄存器eax的值写入内存变量i的地址中
}
在单线程下这些汇编代码执行正常,但是在多线程环境下,这三条汇编指令可能才执行了一句或者两句,就因为cpu时间片结束了导致线程切换,被其他的线程获取值后修改了,导致变量被重复覆盖了
i++是三条指令,所以可能执行完第一条被其他线程抢了时间片,造成结果跟预期不一致问题。
经典的如10个线程每个线程执行1000次i++操作,理想情况下预期是10000,实际上并不一定能到10000,也可能是九千多.
这是AT&T的汇编风格
实现
那有没有什么方法可以避免这一步情况的发生呢,当然,除了常见的加锁外,还有一个粒度很小的方式,那就是原子操作,原子操作需要硬件的支持,不过大部分的电脑都是支持的.
通过汇编指令lock,可以使这三句汇编代码变成一句不可分割的指令(原理是通过lock指令锁住cpu总线(计算机组成原理的知识))
用嵌入汇编实现自增的原子操作
int inc(int* value, int add) {
int old;
__asm__ volatile (
"lock; xaddl %2, %1;" // 指令1:lock; 指令2: xaddl, 操作数占位符:%1, %2
: "=a" (old) // 输出:结果放入通用寄存器eax
: "m" (*value), "a" (add) // 输入:操作数1(内存),操作数2(寄存器eax)
: "cc", "memory" // 编译方式,内存
);
return old;
}
CAS
定义
CAS(Compare and Swap)比较并交换,CAS是一种无锁算法,CAS有3个操作数,传入旧值跟新值让compare去比较内存中已经存在的旧值,如果传入进来的旧值跟内存中的旧值一致那就把传入进来的新增修改,如果不相等则采用自旋的方式拿到内存中的旧值在再次进行比较,自旋可以可以理解为自旋锁机制含义
实现
cas是硬件层面提供的操作
CAS 的基本思路就是,如果这个地址上的值和期望的值相等,则给其赋予新值,否则不做任何事儿,但是要返回原值是多少。循环CAS 就是在一个循环里不断的做cas 操作,直到成功为止。
问题
1.ABA问题
因为CAS 需要在操作值的时候,检查值有没有发生变化,如果没有发生变化则更新,但是如果一个值原来是A,变成了B,又变成了A,那么使用CAS 进行检查时会发现它的值没有发生变化,但是实际上却变化了。
你倒了一杯水放桌子上,干了点别的事,然后同事把你水喝了又给你重新倒了一杯水,你回来看水还在,拿起来就喝,如果你不管水中间被人喝过,只关心水还在,这就是ABA 问题。
如果你是一个讲卫生讲文明的小伙子,不但关心水在不在,还要在你离开的时候水被人动过没有,因为你是程序员,所以就想起了放了张纸在旁边,写上初始值0,别人喝水前麻烦先做个累加才能喝水。
解决方案: 可以采用AtomicMarkableReference,AtomicStampedReference进控制
描述:
1. AtomicStampedReference 是通过 int 类型的版本号,每次修改版本号都会增加,cas操作发现版本号不一致就会返回
1. 而 AtomicMarkableReference 是通过 boolean 型的标识来判断数据是否有更改过。
既然有了 AtomicStampedReference 为啥还需要再提供 AtomicMarkableReference 呢,在现实业务场景中,不关心引用变量被修改了几次,只是单纯的关心是否更改过。
2.开销问题
由于存在并发情况同时修改值,如果值被改过了,就会重新获取内存中的值进行比较,在此期间如果一直修改不成功,会导致做死的循环,会有性能损耗
3.只能保证一个共享变量的原子操作
每次更新操作我只能更新一个值,即一个CAS指令,如果有连个CAS指令那就时独立的了不能保证原子操作
解决方案:
1. 把多个共享变量合并成一个共享变量来操作。
1. 加锁解决
c++语言层面
在c++语言层面提供了cas操作,在头文件#include< atomic>中
- compare_exchange_weak
- compare_exchange_strong
bool compare_exchange_weak( T& expected, T desired,
std::memory_order success,
std::memory_order failure ) noexcept;
bool compare_exchange_weak( T& expected, T desired,
std::memory_order success,
std::memory_order failure ) volatile noexcept;
//(2) (C++11 起)
bool compare_exchange_weak( T& expected, T desired,
std::memory_order order =
std::memory_order_seq_cst ) noexcept;
bool compare_exchange_weak( T& expected, T desired,
std::memory_order order =
std::memory_order_seq_cst ) volatile noexcept;
//(3) (C++11 起)
bool compare_exchange_strong( T& expected, T desired,
std::memory_order success,
std::memory_order failure ) noexcept;
bool compare_exchange_strong( T& expected, T desired,
std::memory_order success,
std::memory_order failure ) volatile noexcept;
//(4) (C++11 起)
bool compare_exchange_strong( T& expected, T desired,
std::memory_order order =
std::memory_order_seq_cst ) noexcept;
bool compare_exchange_strong( T& expected, T desired,
std::memory_order order =
std::memory_order_seq_cst ) volatile noexcept;
而compare_exchange_weak和compare_exchange_strong则是著名的CAS(compare and set)。参数会要求在这里传入期待的数值和新的数值。它们对比变量的值和期待的值是否一致,如果是,则替换为用户指定的一个新的数值。如果不是,则将变量的值和期待的值交换。
weak版本的CAS允许偶然出乎意料的返回(比如在字段值和期待值一样的时候却返回了false),不过在一些循环算法中,这是可以接受的。通常它比起strong有更高的性能。