首页天道酬勤内核同步原语(let’go美国同步英语书)

内核同步原语(let’go美国同步英语书)

admin 11-30 23:23 272次浏览

Go是一种以并发编程闻名的语言。它提供了一系列同步原语供开发人员使用,例如互斥体、RWMutex、WaitGroup、Once、Cond和具有更高抽象级别的Channel。然而,它们实现的基石是原子操作。记住:软件的原子操作离不开硬件指令的支持。本文旨在探索原子操作——比较和交换的实现,以了解Go如何通过硬件指令实现这一过程。

00-1010在看源代码实现之前,我们先来了解CAS。

维基百科定义:CAS是一种原子操作,可以用来实现多线程编程中不间断的数据交换,从而避免多线程同时重写某些数据时,由于执行顺序的不确定性和中断的不可预测性导致的数据不一致。此操作将内存中的值与指定的数据进行比较,当值相同时,用新值替换内存中的数据。

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

bool Cas(int *val,int old,int new)

原子:

if(* val==old){ 0

* val=new

返回1;

} else {

返回0;

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

包装主体

导入(

fmt '

“同步/原子”

)

func main(){ 0

a :=int32(10)

ok :=原子。CompareAndSwapInt32(a,10,100)

fmt。Println(a,ok)

ok=原子。CompareAndSwapInt32(a,10,50)

fmt。Println(a,ok)

}其执行结果如下

$ go run main.go

100%真实

100 false

什么是CAS

CAS从线程层面是非阻塞的,乐观的认为数据更新时不会有其他线程的影响,所以常被称为轻量级乐观锁定。它关注的是并发安全性,而不是并发同步。

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

func(m * Mutex)Lock(){ 0

//Fast path:抓取解锁的互斥体。

如果是原子的。CompareAndSwapInt32(m.state,0,Mutexlocked){ 0

如果种族。已启用{

种族。获取(不安全。指针(m))

}

返回

}

//慢速路径(勾勒出轮廓,以便可以内联快速路径)

m.lockSlow()

}在atomic.compareandswapint 32(m.state,0,mutexLocked)中,m . state表示锁的状态。通过CAS功能,判断此时锁的状态是否空闲(m.state==0),如果是,则锁定(这里mutexlocked为1)。

00-1010也以CompareAndSwapInt32为例,其在sync/atomic/doc.go中定义的函数原型如下

对应于functioncompandswapint 32(addr * int 32,旧的,新的int32) (swappedbool)的程序集代码位于sync/atomic/asm.s中。

文本比较软件32(SB),NOSPLIT,$0

JMP运行时∕内部∕原子CAS (SB)通过指令JMP跳转到其实际实现运行时∕内部∕原子CAS (SB)。这里需要注意的是,由于架构系统的不同,在汇编实现上会有差异。本文以常见的amd64为例,所以我们输入runtime/internal/atomic/ASM _ amd64 . s,汇编代码如下

文本runtime∕internal∕atomic Cas(SB),NOSPLIT,0-17美元

BX,MOVQ ptr 0

MOVL老8(FP),AX

MOVL新12(FP)

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位系统),old和new分别为4个字节,返回参数swapped为1个字节,所以17=8+4+4+1。

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

ptr+0(FP)代表的意思就是ptr从FP偏移0byte处取内容。AX,BX,CX在这里,知道它们是存放数据的寄存器即可。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)

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

SETEQ ret+16(FP) RET

这里,SETEQ 与CMPXCHGL是配合使用的,如果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 : XCHGQCAS : LOCK+ CMPXCHGQAdd : LOCK + XADDQLoad : MOVQStore : XCHGQPointer : 以上指令结合GC 调度

这里大家可能会好奇,Swap和Store会对共享数据做修改,但是为啥它们没有加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

这段话表明,在实现Swap和Store方法时,其实不管是否存在LOCK前缀,在交换操作期间(XCHGQ)将自动实现CPU的锁定协议。

另外我们可以发现,在Load和Store/Swap的实现中,前者没有使用锁定协议,而后者需要。两者结合,那这不就是一种读共享,写独占的思想吗?

Java怎么利用多线程模拟银行系统存钱问题【全球动态加速 PathX】产品简介:原理架构
数字密码学(解析学) ()
相关内容