Go同步原语的基石

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 main

import (
    "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,$0
    JMP 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-17
    MOVQ    ptr+0(FP), BX
    MOVL    old+8(FP), AX
    MOVL    new+12(FP), CX
    LOCK
    CMPXCHGL    CX, 0(BX)
    SETEQ   ret+16(FP)
    RET

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

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

1.png

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指向的内容表示为如下所示。

2.png

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里,也就不用锁定总线,仅锁住缓存行即可。

    LOCK
    CMPXCHGL    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架构开发手册中给出了答案

If 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的实现中,前者没有使用锁定协议,而后者需要。两者结合,那这不就是一种读共享,写独占的思想吗?

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 214,313评论 6 496
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,369评论 3 389
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 159,916评论 0 349
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,333评论 1 288
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,425评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,481评论 1 292
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,491评论 3 412
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,268评论 0 269
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,719评论 1 307
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,004评论 2 328
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,179评论 1 342
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,832评论 4 337
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,510评论 3 322
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,153评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,402评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,045评论 2 365
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,071评论 2 352

推荐阅读更多精彩内容