文章目录
Mutex
临界区
在并发编程中,如果程序中的一部分被并发访问或修改,为了避免并发访问导致的意想不到的结果,这部分程序需要被保护起来,这部分被保护起来的的程序,就叫临界区
临界区可以是一个被共享的资源,或者一个整体的一组共享资源、比如对数据库的访问、某个共享数据结构的操作、对一个IO设备的使用、对一个连接池中的连接调用。
Mutex 互斥锁
mutex 是使用最广泛的同步原语
(并发原语)
同步原语适用场景
- 共享资源:并发地读写共享资源,会出现数据竞争的问题,所以需要Mutex、RWMutex、这样的并发原语来保护
- 任务编排:需要goroutine按照一定的规律执行,而goroutine之间有相互等待或者依赖的顺序关系,通常使用WaitGroup或者Channel来实现
- 消息传递:信息交流以及不同的goroutine之间的线程安全的数据交流,常常使用Channel来实现
注意:
因为Mutex本身没有包含持有这把锁的goroutine的信息,Unlock也不会对此进行检查,所以Unlock可以被任意的goroutine调用释放锁,即时是没有持有这个互斥锁的goroutine的信息。
在使用Mutex的时候必须保证goroutine尽可能不去释放自己未持有的锁,一定要遵循“谁申请,谁释放
”的原则
基本用法
互斥锁Mutex就提供两个方法Lock和Unlock,进入临界区前调用Lock方法,退出临界区调用Unlock方法
当一个goroutine调用Lock方法获得了这个锁的拥有权后,其他请求锁的goroutine就会阻塞在Lock方法的调用上,知道锁被释放并且自己获取到了这个锁的拥有权
func(m *Mutex)Lock()
func(m *Mutex)Unlock()
锁释放后,等待中的goroutine中哪一个会优先获取Mutex?
参考:https://golang.org/src/sync/mutex.go
Mutex 有两种模式 正常和饥饿
- Mutex处于正常模式时,若此时没有新的goroutine与队头goroutine竞争,则队头goroutine获得锁,若有新的goroutine竞争大概率goroutine获得锁
- 当队头goroutine竞争失败1ms后,它会将Mutex调整为饥饿模式。进入饥饿模式后,锁的所有权会直接从解锁goroutine移交给队头goroutine,此时新来的goroutine直接放入队尾
- 当一个goroutine获取锁后,如果发现自己满足下列条件中的任何一个,将锁切换会正常模式
- 它是队列中最后一个
- 它等待锁的时间少于1ms
CAS
CAS 是实现互斥锁和同步原语的基础
CAS 是由CPU在CPU支持的原子操作,其原子性是在硬件层面保证的。
CAS指令将给定的值和一个内存地址中的值进行比较,如果他们是同一个值,就使用新值替换内存地址中的值,这个操作是原子性的。原子性保证这个指令总是基于最新的值进行计算,如果同时有其他线程修改了这个值,那么CAS返回失败
Mutext 架构演进
初版
通过设置一个flag变量,标记当前的锁是否被某个goroutine持有。
如果这个flag的值是1,就代表锁已经被持有,其他竞争的goroutine只能等待。
如果这个flag的值是0,就可以通过cas将这个flag设置为1,标识锁被当前这个goroutine持有了。
// CAS操作,当时还没有抽象出atomic包
func cas(val *int32, old, new int32) bool
func semacquire(*int32)
func semrelease(*int32)
// 互斥锁的结构,包含两个字段
type Mutex struct {
key int32 // 锁是否被持有的标识
sema int32 // 信号量专用,用以阻塞/唤醒goroutine
}
// 保证成功在val上增加delta的值
func xadd(val *int32, delta int32) (new int32) {
for {
v := *val
if cas(val, v, v+delta) {
return v + delta
}
}
panic("unreached")
}
// 请求锁
func (m *Mutex) Lock() {
if xadd(&m.key, 1) == 1 {
//标识加1,如果等于1,成功获取到锁,如果大于1,互斥锁已经被持有
return
}
// goroutine 在这里进行等待,如果被唤醒之后就是直接获取到锁了
semacquire(&m.sema) // 自己没有获取到锁,进入休眠状态等待被唤醒
}
func (m *Mutex) Unlock() {
if xadd(&m.key, -1) == 0 {
// 将标识减去1,如果等于0,则没有其它等待者,如果不等说明有等待者需要去唤醒其他的等待者
return
}
semrelease(&m.sema) // 唤醒其它阻塞的goroutine
}
Mutex 结构体包含两个字段:
- 字段Key:是一个flag。用来标识这个排外锁是否被某个goroutine所持有,如果key大于等于1,说明这个排外锁已经被持有
- 字段sema:是个信号量变量,用来控制等待goroutine的阻塞休眠和唤醒
初版的问题:
请求锁的goroutine会排队等待获取互斥锁。虽然这看起来很公平,但是性能上来看,却不是最优的。因为我们如果能把锁交给正在占用CPU时间片的goroutine的话,就不需要做上下文的切换,在高并发下可能有更好的性能
给新人机会
2011年6月30号调整
Mutex实现:
type Mutex struct {
state int32
sema uint32
}
const (
mutexLocked = 1 << iota // mutex is locked
mutexWoken // 唤醒标志
mutexWaiterShift = iota // 等待者数量
)
Mutex 还是包含两个字段,但是第一个字段已经改为state,并且含义也发生了变化
state 是一个复合型字段,一个字段包含多个意义,这样可以通过尽可能少的内存来实现互斥锁。
- 第一位表示这个锁是否被持有
- 第二位表示是否有唤醒的goroutine
- 剩余的位数表示等待此锁的goroutine数量【2^(32-2)-1 ,基本能满足绝大多数需求】
Lock方法
func (m *Mutex) Lock() {
// Fast path: 幸运case