mutex锁原理
1.mutex锁原理
01.Mutex
1.1 mutex结构体
- 源码包src/sync/mutex.go:Mutex定义了互斥锁的数据结构
- 我们看到Mutex.state是32位的整型变量,内部实现时把该变量分成四份,用于记录Mutex的四种状态。
- 下图展示Mutex的内存布局
Locked
: 表示该Mutex是否已被锁定,0:没有锁定 1:已被锁定。Woken
: 表示是否有协程已被唤醒,0:没有协程唤醒 1:已有协程唤醒,正在加锁过程中。Starving
:表示该Mutex是否处理饥饿状态, 0:没有饥饿 1:饥饿状态,说明有协程阻塞了超过1ms。Waiter
: 表示阻塞等待锁的协程个数,协程解锁时根据此值来判断是否需要释放信号量。
1.2 简单加锁
- 假定当前只有一个协程在加锁,没有其他协程干扰,那么过程如下图所示
- 加锁过程会去判断Locked标志位是否为0,如果是0则把Locked位置1,代表加锁成功
- 从上图可见,加锁成功后,只是Locked位置1,其他状态位没发生变化
1.3 加锁被阻塞
- 假定加锁时,锁已被其他协程占用了,此时加锁过程如下图所示
- 当协程B对一个已被占用的锁再次加锁时,Waiter计数器增加了1
- 此时协程B将被阻塞,直到Locked值变为0后才会被唤醒。
1.4 解锁并唤醒协程
- 假定解锁时,有1个或多个协程阻塞,此时解锁过程如下图所示:
- 协程A解锁过程分为两个步骤,一是把Locked位置0,二是查看到Waiter>0
- 所以释放一个信号量,唤醒一个阻塞的协程,被唤醒的协程B把Locked位置1,于是协程B获得锁。
1.5 自旋过程
- 加锁时,如果当前Locked位为1,说明该锁当前由其他协程持有,尝试加锁的协程并不是马上转入阻塞
- 而是会持续的探测Locked位是否变为0,这个过程即为自旋过程。
- 自旋时间很短,但如果在自旋过程中发现锁已被释放,那么协程可以立即获取锁。此时即便有协程被唤醒也无法获取锁,只能再次阻塞。
- 自旋的好处是,当加锁失败时不必立即转入阻塞,有一定机会获取到锁,这样可以避免协程的切换。
- 自旋条件
- 自旋次数要足够小,通常为4,即自旋最多4次
- CPU核数要大于1,否则自旋没有意义,因为此时不可能有其他协程释放锁
- 协程调度机制中的Process数量要大于1,比如使用GOMAXPROCS()将处理器设置为1就不能启用自旋
- 协程调度机制中的可运行队列必须为空,否则会延迟协程调度
02.原子操作与锁
2.1 什么是原子操作
- 一个或者多个操作在 CPU 执行的过程中不被中断的特性,称为原子性(atomicity) 。
- 这些操作对外表现成一个不可分割的整体,他们要么都执行,要么都不执行,外界不会看到他们只执行到一半的状态。
atomic
包中的原子操作则由底层硬件指令直接提供支持,这些指令在执行的过程中是不允许中断的- 因此原子操作可以在
lock-free
的情况下保证并发安全,并且它的性能也能做到随CPU
个数的增多而线性扩展。
2.2 互斥锁跟原子操作
- 使用目的:互斥锁是用来保护一段逻辑,原子操作用于对一个变量的更新保护。
- 底层实现:
Mutex
由操作系统的调度器
实现,而atomic
包中的原子操作则由**底层硬件指令
**直接提供支持,这些指令在执行的过程中是不允许中断的- 因此原子操作可以在
lock-free
的情况下保证并发安全,并且它的性能也能做到随CPU
个数的增多而线性扩展。
2.3 Mutex实现机制
CAS
(Compare And Swap) 的做法类似操作数据库时常见的乐观锁机制该操作在进行交换前首先确保被操作数的值未被更改,满足此前提条件下才进行交换操作。
其实
Mutex
的底层实现也是依赖原子操作中的CAS
实现的,原子操作的atomic
包相当于是sync
包里的那些同步原语的实现依赖。比如互斥锁
Mutex
的结构里有一个state
字段,其是表示锁状态的状态位。
mutex锁原理
http://coderedeng.github.io/2021/02/23/Go进阶 - mutex锁原理/