经常被误解的 mutex

1,967 阅读3分钟

深入分析 mutex 与 cas 的底层实现 -- CMPXCHGL

mutex 性能太差, 用 cas 吧❓

mutex 性能一定会很差吗?

cas 和 mutex 有什么关系?

最常用的 mutex 是怎么实现的


package main

import "sync"

func main() {
    mu := sync.Mutex{}
    mu.Lock() 
    defer mu.Unlock()
    
}

其中 mu.Lock() 代码如果成功获取到锁, 直接返回, 如果没有获取到🔐, 则进入自旋。其中获取锁的代码

// CompareAndSwapInt32 executes the compare-and-swap operation for an int32 value.
// CompareAndSwapInt32对int32值执行**比较**和**交换**操作。
func CompareAndSwapInt32(addr *int32, old, new int32) (swapped bool)

所以 CompareAndSwapInt32 如函数名一样, 进行比较和交换。 具体是怎么实现的呢?代码中就没有具体的实现了, 他的实现和硬件平台有关的

所以需要使用编译工具把代码编译然后提取出汇编代码。 也可以编译之后使用 objdump 工具,

你怎么知道是过滤 CMPXCHGL 指令的呢? 。。。。 其实编译工具也不是万能的, 就像 unsafe.Pointer 具体实现的汇编代码就没有办法提取出来了。。。

go tool compile -S main.go | grep -C 4 CMPXCHGL > cas_cmp.s
	0x0043 00067 (main.go:6)	MOVQ	AX, CX
	0x0046 00070 ($GOROOT/src/sync/mutex.go:74)	XORL	AX, AX
	0x0048 00072 ($GOROOT/src/sync/mutex.go:74)	MOVL	$1, DX
	0x004d 00077 ($GOROOT/src/sync/mutex.go:74)	LOCK
	`0x004e 00078 ($GOROOT/src/sync/mutex.go:74)	CMPXCHGL	DX, (CX)`
	0x0051 00081 ($GOROOT/src/sync/mutex.go:74)	SETEQ	AL
	0x0054 00084 ($GOROOT/src/sync/mutex.go:74)	TESTB	AL, AL
	0x0056 00086 ($GOROOT/src/sync/mutex.go:74)	JEQ	134
	0x0058 00088 (main.go:9)	LEAQ	sync.(*Mutex).Unlock·f(SB), AX
	0x005f 00095 (main.go:9)	MOVQ	AX, ""..autotmp_7+40(SP)

重点关注第wu行的汇编代码,

现在需要重点关注的是第 4 + 1 行或者说第 6 -1 行的实现(明明知道哪一行为什么不说呢?), 因为年轻人是不讲 5 的。

CMPXCHGL DX, (CX) 也可搜一下 intel CMPXCHG

CMPXCHGL 指令解析

这个指令了一个操作数: EAX。 指令比较 EAX 的值与 DX 的值, 如果相等, 将 CX 的值赋值给 EAX, 并且将标志寄存器 ZF 的标志位设置为 1。 如果不相等, 将 DX 赋值给 EAX, 并且将标志寄存器ZF位置设置为0。 标志寄存器 OF 位置 = 1, 表示溢出; ZF 为零标志(不懂硬件设计);

所以这个指令的大白话就是:

如果 DX == AX:
    AX = (CX)
    ZF = 1
否则:   
    AX = DX
    ZF = 0 

但是这是一条汇编指令, 而不是多条汇编指令.. 这个指令的的设计就是用来交换并且比较两个数, 并且需要在同一个指令中完成

如果没有抢到🔐的 goroutine 就会进入自旋的状态

需要注意

golang 的 mutex 不是可重入的。 如果在同一个 goroutine 中获取同一把🔐将会导致死锁的情况

// 这样的代码将会 deadlock
package main

import "sync"

func main() {
    mu := sync.Mutex{}
    mu.Lock() 
    mu.Lock() 
    defer mu.Unlock()
    
}
  • 互斥条件:一个资源每次只能被一个进程使用, 即在一段时间内某资源仅为一个进程所占有。 如果此时有其他的进程请求该资源, 则请求的进程只能等待。
  • 请求与保持条件: 进程已经保持了至少一个资源, 但有提出了新的资源的请求, 该资源已被其他的进程锁占有, 此时请求进程被阻塞, 但是对自己已经或则的资源保持不释放。
  • 不可剥夺条件: 进程所获得的的资源在未使用完毕之前, 不能被其他的进程强行夺走, 即只能有获得该资源的进程来自己释放
  • 循环等待: 若干进程间形成收尾向量的循环等待资源的关系。

总结

  • mutex 获取🔐资源通过 cas
  • cas 的核心实现是 CMPXCHGL 指令, 具体的实现和硬件平台有关
  • golang 中的 mutex 不是可重入的

参考

intel cmpxchg instruction

求点赞, 代码传送门