Go源码解析之atomic

分享到:

Overview


今天我们来聊聊go的atomic pkg,atomic是go并发编程中最为基础的库。如果说它是go并发编程的基石一点也不为过,像标准库中大家使用率非常高的Mutex, RWMutex,WaitGroup,Once等的实现都依赖于atomic

Atomic简介

atomic提供一系列用于实现同步功能的、底层的,原子的方法:

  1. AddT 系列将增量增加到源值上,并返回新值。
  2. CompareAndSwapT 系列比较两个变量的值,并进行交换。
  3. SwapT系列交换值,并返回旧值。
  4. LoadT 系列获取值。
  5. StoreT 系列更新值。
  6. Value 存储器,支持Load,Store。

这些方法是原子操作,不会被CPU中断,也就说在多个goroutine之间访问是安全的。

比如CompareAndSwapT方法,其实它包含多个步骤,在CPU执行时也是多个命令完成这个功能。

1if *addr == old {
2	*addr = new
3	return true
4}
5return false

那么go是如何让这些方法变成了原子操作呢?我们接着往下看。

刨根问底

为了搞清楚atomic到底是如何工作的,我们以CompareAndSwapInt32为例来分析。我打开了atomic的源代码。

1asm.s
2atomic_test.go
3doc.go
4example_test.go
5race.s
6value.go
7value_test.go

包内的文件数并不多,打开第一个asm.s,我们就看到非常重要的内容。

这是一个go汇编文件,我摘取了部分重要的内容。

 1// +build !race
 2
 3#include "textflag.h"
 4
 5TEXT ·SwapInt32(SB),NOSPLIT,$0
 6	JMP	runtimeinternalatomic·Xchg(SB)
 7
 8// ...略去...
 9
10TEXT ·CompareAndSwapInt32(SB),NOSPLIT,$0
11	JMP	runtimeinternalatomic·Cas(SB)
12
13// ...略去...
14
15TEXT ·AddInt32(SB),NOSPLIT,$0
16	JMP	runtimeinternalatomic·Xadd(SB)
17
18// ...略去...
19
20TEXT ·LoadInt32(SB),NOSPLIT,$0
21	JMP	runtimeinternalatomic·Load(SB)
22
23// ...略去...
24
25TEXT ·StoreInt32(SB),NOSPLIT,$0
26	JMP	runtimeinternalatomic·Store(SB)

// +build !race 这是go的条件编译,表示race时不编译,不是本文重点,欲知更多请查看Go build constraints#include "textflag.h" 引用头文件,定义了一些宏。

下面来到我们的重点 TEXT ·CompareAndSwapInt32(SB),NOSPLIT,$0 定义了CompareAndSwapInt32函数,可以看到它并没有什么逻辑,直接跳转去了runtime∕internal∕atomic·Cas。那么我们就跟过去。

我们查看amd64版本代码stubs.go,看到了函数的声明*func Cas(ptr uint32, old, new uint32) bool,但是并没有函数体。Go还可以这么玩?函数体去哪里了?

经过一番侦查,在asm_amd64.s中发现了汇编实现的函数体。

 1// bool Cas(int32 *val, int32 old, int32 new)
 2// Atomically:
 3//	if(*val == old){
 4//		*val = new;
 5//		return 1;
 6//	} else
 7//		return 0;
 8TEXT runtimeinternalatomic·Cas(SB),NOSPLIT,$0-17
 9	MOVQ	ptr+0(FP), BX
10	MOVL	old+8(FP), AX
11	MOVL	new+12(FP), CX
12	LOCK
13	CMPXCHGL	CX, 0(BX)
14	SETEQ	ret+16(FP)
15	RET

第一行MOVQ ptr到BX寄存器。FP 是go汇编定义的伪寄存器,伪FP寄存器对应的是函数的帧指针,一般用来访问函数的参数和返回值。Go汇编是基于plan9的,MOV的方向和我们常规学习到的相反。

第二行MOVL old值到AX寄存器。

第三行MOVL new值到CX寄存器。

第四行LOCK,这个命令非常陌生。经过一番资料查询了解到Intel® 64 and IA-32 Architectures Software Developer’s Manual,LOCK能将后续的指令变成原子操作,那么后续的CMPXCHGL也将被原子化。

第五行CMPXCHGL CX, 0(BX),将BX的值(ptr)与CX的值(new)比较。如果相等,CX更新到ptr,否者BX更新到AX。

第六行SETEQ ret+16(FP),如果ZF标志位为0,设置1到返回值(FP偏移16位),否者设置0。

第七行RET 函数返回。

这里最重要的是**LOCK**与**CMPXCHGL**两个命令,两条命令组合完成了Cas操作。
这是CPU支持的原子操作。关于CPU的LOCK指令后续我们单独介绍。

atomic中其他方法实现原子操作的方案基本与此一致,在此就不赘述了,有兴趣的童鞋可以自己研究一下。

参考文档

  1. 探索 Golang 一致性原语
comments powered by Disqus