Go是一门以并发编程见长的语言,它提供了一系列的同步原语方便开发者使用,例如sync包下的MutexRWMutexWaitGroupOnceCond,以及抽象层级更高的Channel。但是,它们的实现基石是原子操作。需要记住的是:软件原子操作离不开硬件指令的支持。本文拟通过探讨原子操作——比较并交换(compare and swap, CAS)的实现,来理解Go是如何借助硬件指令来实现这一过程的。

什么是CAS

看源码实现之前,我们先理解一下CAS。

维基百科定义:CAS是原子操作的一种,可用于在多线程编程中实现不被打断的数据交换操作,从而避免多线程同时改写某一数据时由于执行顺序不确定性以及中断的不可预知性产生的数据不一致问题。该操作通过将内存中的值与指定数据进行比较,当数值一样时将内存中的数据替换为新的值。

CAS的实现思想可以用以下伪代码表示

bool Cas(int *val, int old, int new)Atomically:if(*val == old){*val = new;return 1;} else {return 0;}

sync/atomic/doc.go中,定义了一系列原子操作函数原型。以CompareAndSwapInt32为例,有以下代码

package mainimport ("fmt""sync/atomic"
)func main() {a := int32(10)ok := atomic.CompareAndSwapInt32(&a, 10, 100)fmt.Println(a, ok)ok = atomic.CompareAndSwapInt32(&a, 10, 50)fmt.Println(a, ok)
}

它的执行结果如下

$ go run main.go
100 true
100 false

CAS能做什么

CAS从线程层面来说,它是非阻塞的,其乐观地认为在数据更新期间没有其他线程影响,因此也常常被称为是一种轻量级的乐观锁。它关注的是并发安全,而并非并发同步。

在文章开头时,我们就已经提到原子操作是实现上层同步原语的基石。以互斥锁为例,为了方便理解,我们在这里将它的状态定义为0和1,0代表目前该锁空闲,1代表已被加锁。那么,这个时候,CAS就是管理状态的最佳选择。以下是sync.MutexLock方法的部分实现代码。

func (m *Mutex) Lock() {// Fast path: grab unlocked mutex.if atomic.CompareAndSwapInt32(&m.state, 0, mutexLocked) {if race.Enabled {race.Acquire(unsafe.Pointer(m))}return}// Slow path (outlined so that the fast path can be inlined)m.lockSlow()
}

atomic.CompareAndSwapInt32(&m.state, 0, mutexLocked)中,m.state代表锁的状态,通过CAS函数,判断锁此时的状态是否空闲(m.state==0),是,则对其加锁(这里mutexLocked的值为1)。

源码解读

同样还是以CompareAndSwapInt32为例,它在sync/atomic/doc.go中定义的函数原型如下

func CompareAndSwapInt32(addr *int32, old, new int32) (swapped bool)

对应的汇编代码位于sync/atomic/asm.s

TEXT ·CompareAndSwapInt32(SB),NOSPLIT,$0JMP runtime∕internal∕atomic·Cas(SB)

通过指令JMP,跳转到它的实际实现runtime∕internal∕atomic·Cas(SB)。这里需要注意的是,由于架构体系差异,其汇编实现也会存在差别。在本文,我们就以常见的amd64为例,因此进入runtime/internal/atomic/asm_amd64.s,汇编代码如下

TEXT runtime∕internal∕atomic·Cas(SB),NOSPLIT,$0-17MOVQ    ptr+0(FP), BXMOVL    old+8(FP), AXMOVL    new+12(FP), CXLOCKCMPXCHGL    CX, 0(BX)SETEQ   ret+16(FP)RET

Go的汇编是基于 Plan9 的,我想是因为Ken Thompson(他是Plan 9操作系统的核心成员)吧。如果你不熟悉Plan 9,看到这段汇编可能比较懵。小菜刀觉得没必要花过多时间去学懂,因为它很复杂且另类,同时涉及到很多硬件知识。不过如果只是要求看懂简单的汇编代码,稍微研究下还是能够做到的。

由于本文的重点并不是plan 9,所以这里就只解释上述汇编代码的含义。

图片

atomic.Cas(SB)的函数原型为func CompareAndSwapInt32(addr *int32, old, new int32) (swapped bool),其入参addr为8个字节(64位系统),oldnew分别为4个字节,返回参数swapped为1个字节,所以17=8+4+4+1。

FP(Frame pointer: arguments and locals),它是伪寄存器,用来表示函数参数与局部变量。其通过symbol+offset(FP)的方式进行使用。在本函数中,我们可以把FP指向的内容表示为如下所示。

图片

ptr+0(FP)代表的意思就是ptr从FP偏移0byte处取内容。AXBXCX在这里,知道它们是存放数据的寄存器即可。MOV X Y所做的操作是将X上的内容复制到Y上去,MOV后缀L表示“长字”(32位,4个字节),Q表示“四字”(64位,8个字节)。

 MOVQ     ptr+0(FP), BX  // 第一个参数addr命名为ptr,放入BP(MOVQ,完成8个字节的复制)MOVL     old+8(FP), AX  // 第二个参数old,放入AX(MOVL,完成4个字节的复制)MOVL     new+12(FP), CX // 第三个参数new,放入CX(MOVL,完成4个字节的复制)

重点来了,LOCK指令。这里参考 Intel 的64位和IA-32架构开发手册

Causes the processor’s LOCK# signal to be asserted during execution of the accompanying instruction (turns the instruction into an atomic instruction). In a multiprocessor environment, the LOCK# signal ensures that the processor has exclusive use of any shared memory while the signal is asserted.

在多处理器环境中,指令前缀LOCK能够确保,在执行LOCK随后的指令时,处理器拥有对任何共享内存的独占使用。

LOCK:是一个指令前缀,其后必须跟一条“读-改-写”性质的指令,它们可以是ADD, ADC, AND, BTC, BTR, BTS, CMPXCHG, CMPXCH8B, CMPXCHG16B, DEC, INC, NEG, NOT, OR, SBB, SUB, XOR, XADD,  XCHG。该指令是一种锁定协议,用于封锁总线,禁止其他 CPU 对内存的操作来保证原子性。

在汇编代码里给指令加上 LOCK 前缀,这是CPU 在硬件层面支持的原子操作。但这样的锁粒度太粗,其他无关的内存操作也会被阻塞,大幅降低系统性能,核数越多愈发显著。

为了提高性能,Intel 从 Pentium 486 开始引入了粒度较细的缓存锁:MESI协议(关于该协议,小菜刀在之前的文章《CPU缓存体系对Go程序的影响》有详细介绍过)。此时,尽管有LOCK前缀,但如果对应数据已经在 cache line里,也就不用锁定总线,仅锁住缓存行即可。

    LOCKCMPXCHGL    CX, 0(BX)

CMPXCHGLL代表4个字节。该指令会把AX(累加器寄存器)中的内容(old)和第二个操作数(0(BX))中的内容(ptr所指向的数据)比较。如果相等,则把第一个操作数(CX)中的内容(new)赋值给第二个操作数。

 SETEQ    ret+16(FP)RET

SETEQCMPXCHGL是配合使用的,如果CMPXCHGL中比较结果是相等的,则设置ret(即函数原型中的swapped)为1,不等则设置为0。RET代表函数返回。

总结

本文探讨了atomic.CompareAndSwapInt32是如何通过硬件指令LOCK实现原子性操作的封装。但要记住,在不同的架构平台,依赖的机器指令是不同的,本文仅研究的是amd64下的汇编实现。

在Go提供的原子操作库atomic中,CAS还有许多有用的原子方法,它们共同筑起了Go同步原语体系的基石

func SwapIntX(addr *intX, new intX) (old intX)
func CompareAndSwapIntX(addr *intX, old, new intX) (swapped bool)
func AddIntX(addr *intX, delta intX) (new intX)
func LoadIntX(addr *uintX) (val uintX)
func StoreIntX(addr *intX, val intX)
func XPointer(addr *unsafe.Pointer, val unsafe.Pointer)

那么它们是如何实现的?小菜刀将它们实现的关键指令总结如下。

  • Swap : XCHGQ

  • CAS : LOCK+ CMPXCHGQ

  • Add : LOCK + XADDQ

  • Load : MOVQ

  • Store : XCHGQ

  • Pointer : 以上指令结合GC 调度

这里大家可能会好奇,SwapStore会对共享数据做修改,但是为啥它们没有加LOCK,小菜刀对此也同样疑惑。好在 Intel 的64位和IA-32架构开发手册中给出了答案:

1If a memory operand is referenced, the processor’s locking protocol is automatically implemented for the duration of the exchange operation, regardless of the presence or absence of the LOCK prefix or of the value of the IOPL

在实现SwapStore方法时,其实不管是否存在LOCK前缀,在交换操作期间(XCHGQ)将自动实现CPU的锁定协议。

除此之外,在LoadStore/Swap的实现中,前者没有使用锁定协议,而后者需要。两者结合,那这不就是一种读共享,写独占的思想吗?

以上分享来自号主:机器玲砍菜刀,喜欢硬核文章的可以关注他的公众号

也欢迎关注我的公众号,会为大家持续带来精彩的技术文章

给大家介绍一下实现Go并发同步原语的基石相关推荐

  1. Go 学习笔记(66)— Go 并发同步原语(sync.Mutex、sync.RWMutex、sync.Once)

    1. 竞态条件 一旦数据被多个线程共享,那么就很可能会产生争用和冲突的情况.这种情况也被称为竞态条件(race condition),这往往会破坏共享数据的一致性. 举个例子,同时有多个线程连续向同一 ...

  2. Go 语言编程 — 并发 — 同步原语与锁

    目录 文章目录 目录 协程锁 协程锁 协程锁主要用于保证在执行 goroutine 的时候不阻塞 M. 举例:任务 A 需要修改 Z,任务 B 也需要修改 Z.如果是串行系统,A 执行完了,再执行B, ...

  3. MySQL系列:innodb源代码分析之线程并发同步机制

    innodb是一个多线程并发的存储引擎,内部的读写都是用多线程来实现的,所以innodb内部实现了一个比較高效的并发同步机制. innodb并没有直接使用系统提供的锁(latch)同步结构,而是对其进 ...

  4. 线程互斥与同步 在c#中用mutex类实现线程的互斥_Golang 并发编程与同步原语

    5.1 同步原语与锁 · 浅谈 Go 语言实现原理​draveness.me 当提到并发编程.多线程编程时,我们往往都离不开『锁』这一概念,Go 语言作为一个原生支持用户态进程 Goroutine 的 ...

  5. guava 并发同步_Google Guava –与Monitor同步

    guava 并发同步 Google Guava项目是每个Java开发人员都应该熟悉的库的集合. Guava库涵盖了I / O,集合,字符串操作和并发性. 在这篇文章中,我将介绍Monitor类. Mo ...

  6. java 并发 同步信号_Java并发教程–信号量

    java 并发 同步信号 这是我们将要进行的Java并发系列的第一部分. 具体来说,我们将深入研究Java 1.5及更高版本中内置的并发工具. 我们假设您对同步和易失性关键字有基本的了解. 第一篇文章 ...

  7. Golang 并发编程之同步原语

    当提到并发编程.多线程编程时,我们往往都离不开『锁』这一概念,Go 语言作为一个原生支持用户态进程 Goroutine 的语言,也一定会为开发者提供这一功能,锁的主要作用就是保证多个线程或者 Goro ...

  8. java多线程问题_【java 多线程】多线程并发同步问题及解决方法

    一.线程并发同步概念 线程同步其核心就在于一个"同".所谓"同"就是协同.协助.配合,"同步"就是协同步调昨,也就是按照预定的先后顺序进行运 ...

  9. java的知识点32——多线程 并发同步的 性能分析、快乐影院  订票操作

    多线程  并发  同步  性能分析 /*** 线程安全: 在并发时保证数据的正确性.效率尽可能高* synchronized* 1.同步方法* 2.同步块* @author Administrator ...

最新文章

  1. JS阻止事件冒泡的3种方法,以及他们之间的不同
  2. 泰勒公式推导过程_论泰勒级数在机器学习家庭中的地位
  3. [NOIp 2013]货车运输
  4. python安装不了jupyter_求救 python3.8安装jupyter报错无“winpty.h”
  5. Android学习—补间动画(渐变动画)
  6. 聊聊架构设计做些什么来谈如何成为架构师
  7. mysql事务变量_mysql学习四之事务、变量、触发器、函数、存储过程
  8. 2016陕西省ACM 热身体B 种类并查集
  9. php javascript对象,JavaScript 对象
  10. 洛谷P4325、P4413题解(Java语言描述)
  11. 第6-5课:五子棋游戏的相关算法
  12. 酷派删除android系统软件,Coolpad酷派8720L哪些系统软件可以删除(精简列表)
  13. 2018年 - 年终总结
  14. clustalw序列比对_COBALT:NCBI在线蛋白多序列比对(比ClustalW还强大的工具)
  15. Javascript实现图片轮播效果。
  16. 胡说八道设计模式—观察者模式
  17. 什么是OXC(全光交叉)?
  18. 简单的《找不同汉字版》,来考考你的眼力吧
  19. 视觉3D感知(一):初步认识
  20. Qt安装包百度网盘下载分享

热门文章

  1. 2015手机病毒暴涨16倍 每天70万人次连接风险WiFi
  2. 走近伏羲,谈5000节点集群调度与性能优化
  3. ExecutorService中submit和execute的区别转
  4. 腾讯2014年实习生招聘笔试面试经历
  5. 解决C# Repeater内嵌Repeater 数据绑定,以及第二次层Repeater的ItemDataBound事件怎么处理...
  6. 维基百科(wikipedia)数据下载(含地理数据)
  7. 防止网页后退--禁止缓存
  8. IIS 7 托管管道模式 经典模式(Classic) 集成模式(Integrated) 分析与理解
  9. 应对深度学习人才缺口,百度黄埔学院发起深度学习架构师培养计划...
  10. Vue实战:音乐播放器(一) 页面效果