锁 和 CPU CAS 原子操作

提示:具体围绕着CPU CAS场景讲,代码写在最后

问题简介

问题1:
在我们写代码时定义了一个变量,给它赋值的同时
去查询该变量,会发现值不对.
string 有乱码
对象结构缺少部分数据
为什么会发生这种情况呢?
因为你在并发修改和读取时
会发生一部分字节并未完全赋值
所以读出来的数据只是一部分.

问题2:
当多线程时,为了控制并发安全,锁性能太慢咋办.
下面的描述部分都是以问题1为主
嫌锁性能太慢就不要用锁,因为它阻塞在那里,它也快不了。
换一种不阻塞的方式就好了.

问题举例

问题1:其实都一样,string、结构 操作系统在高低位进行赋值时
都有可能会存在没有赋完的情况,这个时候取出来都会有问题.
这种情况的发生是因为cpu本身是乱序执行的
类型的赋值(除了基本类型)都不是原子性,比如go、java
除非类型的本身有保证原子性的机制

疑惑

问题1:有些朋友估计还是比较疑惑的,为啥自己写了这么久的程序,
没有遇到过呢?因为大部分的操作是由固定变量+同步执行来完成的.
就算是多线程的异步操作,在获取数值时去改变数据,也不会那么容易重现
因为cpu的存储指令操作是特别快的,瞬间就完成了
虽然是极少数发生的情况,但是不可忽略,因为许多信息、数据对于企业
都是特别重要的,当真的发生问题时,再想去用补偿机制挽回,
往往不会那么容易.

解决方案

可能朋友最先想到的就是锁了,锁肯定是能解决,但性能低效,
还有可能会发生死锁等问题.
这里推荐的就是本篇所要说的CPU CAS操作

CPU cas

分为两种模式:总线锁定、缓存锁定

总线锁定:
cpu在下达指令操作时,都会由一个"总线"来操作,
当我们的具体操作由总线来实现时,就能实现其他操作
无法处理这个共享变量,但这样会增大性能负担,
因为其他请求都阻塞到了

缓存锁定:
现在处理器差不多都提供缓存锁定机制了,
它的机制是在你修改共享变量时,会预测你修改得值
有没有被修改过,如果被修改则会重新访问,否则就直接修改掉

CPU cas 为什么比锁得性能快

这个其实就是相对性的了,小结构得情况下,
可能会比锁快个几十倍甚至更高,
但是如果是map等一些复杂大型的结构,
缓存锁定所复制map和存储map的开销也非常大.
所以需要看是什么类型。

快的具体原因:

cas操作类似于乐观锁那种,当后续请求一直不通过时,
会不停的请求访问。

锁:可以粗略的理解为悲观锁,它会有一个线程沉睡唤醒的概念,
当请求不通过时就会沉睡,需要通过cpu来进行唤醒再次执行,
这中间就有cpu上下文切换所带来的所资源消耗,
这个消耗是比较耗时的.

当然要是请求过多cpu cas操作也会特别消耗性能,但锁也是一样

CPU cas 操作

最常用的两种:
1.解决变量并发操作问题
2.可以实现类似于锁的效果,但无法实现锁阻塞效果,
如果是单例模式直接退出的情况,那这个速度就非常nice

上代码:

锁 和 CPU cas 性能操作对比

func TestCas() {

value := test{}

//atomic操作 实现cpu cas操作
b := atomic.Value{}
b.Store(value)

//group确保携程全部跑完在退出
var wg sync.WaitGroup
//当前时间
t1 := time.Now()

//计数
i := 1
for i < 10 {
wg.Add(1)
//开启携程异步来跑
go func() {
defer wg.Done()
//mutex.Lock()
i++
//使用cas 赋值
b.Store(test{i, string(i), string(i), i, i})
time.Sleep(1 * time.Millisecond)
//mutex.Unlock()
}()
}
for i > 0 {
wg.Add(1)
//开启携程异步来跑
go func() {
defer wg.Done()
//mutex.Lock()
i--
time.Sleep(1 * time.Millisecond)
value = b.Load().(test)

//mutex.Unlock()
}()
}
wg.Wait()
fmt.Println(time.Now().Sub(t1).Seconds())

}

func TestLock() {

//锁定义
var mutex sync.Mutex

//group确保携程全部跑完在退出
var wg sync.WaitGroup
//当前时间
t1 := time.Now()

//计数
i := 1
for i < 10 {
wg.Add(1)
//开启携程异步来跑
go func() {
defer wg.Done()
//锁
mutex.Lock()
i++
//忽略修改某个值
time.Sleep(1 * time.Millisecond)
//解锁
mutex.Unlock()
}()
}
for i > 0 {
wg.Add(1)
//开启携程异步来跑
go func() {
defer wg.Done()
//锁
mutex.Lock()
i--
//忽略取某个值
time.Sleep(1 * time.Millisecond)
//解锁
mutex.Unlock()
}()
}
wg.Wait()
fmt.Println(time.Now().Sub(t1).Seconds())

}

TestCas用时0.001332754秒
TestLock用时9.75207781秒
由此可见锁在某些情况下是比CPU cas原子性操作慢很多的

并发操作字符串

func Test() {
test := "test"
//开启携程异步操作
go func() {
i := 1
for {
i++
//这个取余数的效果是为了让test有值的转换每执行一次值都会有变动
if i%2 == 1 {
//给了20个王
test = "王王王王王王王王王王王王王王王王王王王王"
} else {
//把test在还给变量
test = "test"
}
time.Sleep(1 * time.Millisecond)
}
}()
//开启携程异步操作
go func() {
for {
time.Sleep(1 * time.Millisecond)
//把test的值取出来
v := test
//因为以上写的逻辑 test最终的结果只有 test 或者是 20个王
//但是由于是并发操作,所以可能20个王并不是完整的
//这边输出的就是如果你包含了一个王但是没有满足20个王的情况,那么就会输出出来
if strings.Contains(cast.ToString(v), "王") && !strings.Contains(cast.ToString(v), "王王王王王王王王王王王王王王王王王王王王") {
fmt.Println(v)
}
}
}()
}
//输出了 王�
//说明并发操作的原因导致test有乱码且不完整
//我们改程 CPU cas操作
func Test() {
//定义 atomic
test := atomic.Value{}
//使用 CPU cas赋值
test.Store("test")
//开启携程异步操作
go func() {
i := 1
for {
i++
//这个取余数的效果是为了让test有值的转换每执行一次值都会有变动
if i%2 == 1 {
//给了20个王
test.Store("王王王王王王王王王王王王王王王王王王王王")
} else {
//把test在还给变量
test.Store("test")
}
time.Sleep(1 * time.Millisecond)
}
}()
//开启携程异步操作
go func() {
for {
time.Sleep(1 * time.Millisecond)
//把test的值取出来
v := test.Load().(string)
//因为以上写的逻辑 test最终的结果只有 test 或者是 20个王
//但是由于是并发操作,所以可能20个王并不是完整的
//这边输出的就是如果你包含了一个王但是没有满足20个王的情况,那么就会输出出来
if strings.Contains(cast.ToString(v), "王") && !strings.Contains(cast.ToString(v), "王王王王王王王王王王王王王王王王王王王王") {
fmt.Println(v)
}
}
}()
}
使用 CPU cas 控制后,就不会出现这种情况了

结构就不做展示了,和string的效果类似,

就是结构里有些属性已经修改,有些则没有

阅读剩余
THE END