我不想卷,我是被逼的 在做了几年前端之后,发现互联网行情比想象的差,不如赶紧学点后端知识,被裁之后也可接个私活不至于饿死。学习两周Go,如盲人摸象般不知重点,那么重点
在做了几年前端之后,发现互联网行情比想象的差,不如赶紧学点后端知识,被裁之后也可接个私活不至于饿死。学习两周Go,如盲人摸象般不知重点,那么重点谁知道呢?肯定是使用Go的后端工程师,那便利用业余时间找了几个老哥对练一下。其中一位问道在利用多个goroutine发送请求拿到结果之后如果进行销毁。是个好问题,研究了一下需要利用Context,而我一向喜欢研究源码,继续深挖发现细节非常多,于是乎有此这篇文章。
有句话叫做初出茅庐天下无敌,再练三年寸步难行。本着不服输精神回来研究了一下这个问题,很简单需要使用Go提供的Context,api使用起来也很简单,但是我一向喜欢刨根问底,于是乎研究Context源码发现互斥锁(Mutex)、原子操作(atomic),研究atomic发现CAS,研究CAS发现了java的自旋锁、偏向锁、轻量级锁、重量级锁,研究锁发现Disruptor,研究Disruptor发现CPU伪共享、MESI协议、内存屏障。



- Modified,已修改
- Exclusive,独占
- Shared,共享
- Invalidated,已失效



type Value struct { v interface{} }除此之外还有一个ifaceWords类型,这其实是对应于空interface的内部表示形式,主要为了得到其中的typ和data两个字段:
type ifaceWords struct {
typ unsafe.Pointer
data unsafe.Pointer
}
这里用的是unsafe.Pointer它是可以直接操作内存,因为如果两种类型具有相同的内存结构,其实可以利用unsafe.Pointer来让两种类型的指针相互转换,来实现同一份内存的的不用解读。这里可以内部JavaScript中的ArrayBuffer可以被转化成DataView或者不同的TypedArray进行不同的解读。下面举了一个[]byte和string的例子,因为Go语言类型系统禁止他俩互转,但是可以利用unsafe.Pointer来绕过类型系统检查,直接转换。
bytes := []byte{104, 101, 108, 108, 111} p := unsafe.Pointer(&bytes) //强制转换成unsafe.Pointer,编译器不会报错 str := *(*string)(p) //然后强制转换成string类型的指针,再将这个指针的值当做string类型取出来 fmt.Println(str) //输出 "hello"下面来看下Store函数的代码:
func (v *Value) Store(x interface{}) { if x == nil { panic("sync/atomic: store of nil value into Value") } // 通过unsafe.Pointer将现有的和要写入的值分别转成ifaceWords类型, // 这样我们下一步就可以得到这两个interface{}的原始类型(typ)和真正的值(data) vp := (*ifaceWords)(unsafe.Pointer(v)) // Old value xp := (*ifaceWords)(unsafe.Pointer(&x)) // New value // 这里开始利用CAS来自旋了 for { // 通过LoadPointer这个原子操作拿到当前Value中存储的类型 typ := LoadPointer(&vp.typ) if typ == nil { // typ为nil代表Value实例被初始化,还没有被写入数据,则进行初始写入; // 初始写入需要确定typ和data两个值,非初始写入只需要更改data // Attempt to start first store. // Disable preemption so that other goroutines can use // active spin wait to wait for completion; and so that // GC does not see the fake type accidentally. // 获取runtime总当前P(调度器)并设置禁止抢占,使得goroutine执行当前逻辑不被打断以便尽快完成,同时这时候也不会发生GC // pin函数会将当前 goroutine绑定的P, 禁止抢占(preemption) 并从 poolLocal 池中返回 P 对应的 poolLocal runtime_procPin() // 使用CAS操作,先尝试将typ设置为^uintptr(0)这个中间状态。 // 如果失败,则证明已经有别的线程抢先完成了赋值操作,那它就解除抢占锁,然后重新回到 for 循环第一步进行自旋 // 回到第一步后,则进入到if uintptr(typ) == ^uintptr(0)这个逻辑判断和后面的设置StorePointer(&vp.data, xp.data) if !CompareAndSwapPointer(&vp.typ, nil, unsafe.Pointer(^uintptr(0))) { // 设置成功则将P恢复原样 runtime_procUnpin() continue } // Complete first store. // 这里先写data字段在写typ字段,因为这个两个单独都是原子的 // 但是两个原子放在一起未必是原子操作,所以先写data字段,typ用来做判断 StorePointer(&vp.data, xp.data) StorePointer(&vp.typ, xp.typ) runtime_procUnpin() return } if uintptr(typ) == ^uintptr(0) { // 这个时候typ不为nil,但可能为^uintptr(0),代表当前有一个goroutine正在写入,还没写完 // 我们先不做处理,保证那个写入线程操作的原子性 // First store in progress. Wait. // Since we disable preemption around the first store, // we can wait with active spinning. continue } // First store completed. Check type and overwrite data. if typ != xp.typ { // atomic.Value第一确定类型之后,后续都不能改变 panic("sync/atomic: store of inconsistently typed value into Value") } // 非第一次写入,则利用StorePointer这个原子操作直接写入。 StorePointer(&vp.data, xp.data) return } }这个逻辑的主要思想就是,为了完成多个字段的原子性写入,我们可以抓住其中的一个字段,以它的状态来标志整个原子写入的状态。这个想法在 TiDB 的事务实现中叫Percolator模型,主要思想也是先选出一个primaryRow,然后所有的操作也是以primaryRow的成功与否作为标志。 atomic.Value的读取则简单很多。
func (v *Value) Load() (x interface{}) { vp := (*ifaceWords)(unsafe.Pointer(v)) typ := LoadPointer(&vp.typ) // 原子性读 // 如果当前的typ是 nil 或者^uintptr(0),那就证明第一次写入还没有开始,或者还没完成,那就直接返回 nil (不对外暴露中间状态)。 if typ == nil || uintptr(typ) == ^uintptr(0) { // First store not yet completed. return nil } // 否则,根据当前看到的typ和data构造出一个新的interface{}返回出去 data := LoadPointer(&vp.data) xp := (*ifaceWords)(unsafe.Pointer(&x)) xp.typ = typ xp.data = data return }
内存屏障 在编译器层面也会对我们写的代码做优化,导致CPU看到的指令顺序跟我们写的代码术顺序并不完全是一致的,这就也会导致多核执行情况下,数据不一致问题。而内存屏障也是解决这些问题的一种手段,各个语言封装底层指令,强制CPU指令按照代码写的顺序执行。 在上文中可以看到为提供缓冲命中和减少与内存通信频率,CPU做了各种优化策略,有的会给我们带来一些问题,比如某个核心更新了数据之后,如果没有进行原子操作会导致各个核心在L1中的数据不一致问题。内存屏障另一个作用是强制更新CPU的缓存,比如一个写屏障指令会把这个屏障前写入的数据更新到缓存中,这样任何后面试图读取该数据的线程都将得到最新值。 一般来说读写屏障是一起使用的,比如在java中,如果用volatile来修饰一个字段,Java内存模型将在写操作后插入一个写屏障指令,而在读操作前插入一个读屏障指令。所以如果对一个volatile字段进行操作,一旦完成写入,任何访问这个字段的线程都会得到最新值;在写入前volatile字段前,会被保证所有之前发生的事情都已经发生,并且任何更新过的数据值也是可见的,因为内存屏障会把之前的写入值都更新到缓存。 实际中Disruptor的Sequence就是利用了内存屏障这点(新版本已经不用了https://github.com/LMAX-Exchange/disruptor/blob/master/src/main/java/com/lmax/disruptor/Sequence.java)




/**@param delta the value to add * @return the previous value */ * Atomically adds the given value to the current value. * * public final int getAndAdd(int delta) { for (;;) { int current = get(); int next = current + delta; if (compareAndSet(current, next)) return current; } } /**@code ==} the expected value. * * @param expect the expected value * @param update the new value * @return true if successful. False return indicates that * the actual value was not equal to the expected value. */ * Atomically sets the value to the given updated value * if the current value { public final boolean compareAndSet(int expect, int update) { return unsafe.compareAndSwapInt(this, valueOffset, expect, update); }
从Go的Context到atomic.Value,再到去学习CAS,再到发现各种锁,然后找锁存在的意义找到CPU层,整个过程其实是带着问题自上向下的,而文章是我在理解这些概念原理之后,自下向上一步步解答其中的问题,希望没有后端经验的前端同学能够看懂。原来想把整个Disruptor和Go的Context全部写完,现在已经十点多了,不卷洗洗睡,剩下文章等下周把。 参考资料 本文大量引用了相关参考资料的图片和语言,尤其是CPU硬件部分图片大部分来自于小林coding(https://xiaolincoding.com/os/1_hardware/cpu_mesi.html)的图片。版权问题请与我联系,侵删。
- 深入理解Go Context:https://article.itxueyuan.com/39dbvb
- context源码:https://github.com/golang/go/blob/master/src/context/context.go
- 聊一聊Go的Context上下文:https://studygolang.com/articles/28726
- go context详解:https://www.cnblogs.com/juanmaofeifei/p/14439957.html
- Go语言Context(上下文):http://c.biancheng.net/view/5714.html
- atomic原理以及实现:https://blog.csdn.net/u010853261/article/details/103996679
- atomic前世今生:https://blog.betacat.io/post/golang-atomic-value-exploration/
- CAS乐观锁:https://blog.csdn.net/yanluandai1985/article/details/82686486
- CAS乐观锁:https://blog.csdn.net/nrsc272420199/article/details/105032873
- 偏向锁、轻量级锁、重量级锁、自旋锁原理:https://blog.csdn.net/qq_43141726/article/details/118581304
- 自旋锁,偏向锁,轻量级锁,重量级锁:https://www.jianshu.com/p/27290e67e4d0
- CAS与自旋锁:https://blog.csdn.net/weixin_52904390/article/details/113700649
- 自旋锁、CAS、悲观锁、乐观锁:https://blog.csdn.net/weixin_45102619/article/details/120605691
- Go并发面试总结:https://www.iamshuaidi.com/8942.html
- 高性能队列-Disruptor:https://tech.meituan.com/2016/11/18/disruptor.html
- 锁与原子操作的关系:https://www.cnblogs.com/luconsole/p/4944304.html
- 多线程顺序打印:https://www.cnblogs.com/lazyegg/p/13900847.html
- 如何实现一个乐观锁:https://zhuanlan.zhihu.com/p/137818729
- disruptor与内存屏障:http://ifeve.com/disruptor-memory-barrier/
- Java volatile的作用:http://www.51gjie.com/java/574.html
- 浅谈原子操作:https://zhuanlan.zhihu.com/p/333675803
- sync.Pool设计思路:https://blog.csdn.net/u010853261/article/details/90647884
