Mutex 学习笔记

Mutex

Mutex 互斥锁

临界区

在并发编程中,如果程序中的一部分被并发访问或修改,为了避免并发访问导致的意想不到的结果,这部分程序需要被保护起来,这部分被保护起来的的程序,就叫临界区

临界区可以是一个被共享的资源,或者一个整体的一组共享资源、比如对数据库的访问、某个共享数据结构的操作、对一个IO设备的使用、对一个连接池中的连接调用。

img

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 有两种模式 正常和饥饿

  1. Mutex处于正常模式时,若此时没有新的goroutine与队头goroutine竞争,则队头goroutine获得锁,若有新的goroutine竞争大概率goroutine获得锁
  2. 当队头goroutine竞争失败1ms后,它会将Mutex调整为饥饿模式。进入饥饿模式后,锁的所有权会直接从解锁goroutine移交给队头goroutine,此时新来的goroutine直接放入队尾
  3. 当一个goroutine获取锁后,如果发现自己满足下列条件中的任何一个,将锁切换会正常模式
    1. 它是队列中最后一个
    2. 它等待锁的时间少于1ms

CAS

CAS 是实现互斥锁和同步原语的基础

CAS 是由CPU在CPU支持的原子操作,其原子性是在硬件层面保证的。

CAS指令将给定的值和一个内存地址中的值进行比较,如果他们是同一个值,就使用新值替换内存地址中的值,这个操作是原子性的。原子性保证这个指令总是基于最新的值进行计算,如果同时有其他线程修改了这个值,那么CAS返回失败

Mutext 架构演进

img

初版

通过设置一个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的阻塞休眠和唤醒

img

初版的问题:

请求锁的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 ,基本能满足绝大多数需求】

img

Lock方法

func (m *Mutex) Lock() {
   
   // Fast path: 幸运case
  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值