sync.Mutex 的简单使用
一、前导概念
竞态条件
锁是一种并发编程中的同步原语(Synchronization Primitives),它能保证多个 Goroutine 在访问同一片内存时不会出现竞争条件(Race condition)等问题。
竞态条件:一旦数据被多个线程共享,那么就很可能会产生争用和冲突的情况。这种情况也被称为竞态条件(race condition),这往往会破坏共享数据的一致性。
临界区
多个并发运行的线程对这个共享资源的访问是完全串行的。只要一个代码片段需要实现对共享资源的串行化访问,就可以被视为一个临界区(critical section)
二、sync.Mutex
sync.Mutex 是一个互斥锁,保证同时只有一个Goroutine可以访问共享资源。Mutex 类型的锁和 Goroutine 无关,可以由不同的 Goroutine 加锁和解锁。也可以为其他结构体的字段,零值为解锁状态。
互斥锁可以看做是进入临界区的令牌,在进入之前申请
2.1、结构和方法
结构体
1 | type Mutex struct { |
方法列表
方法名 | 描述 |
---|---|
(m *Mutex) Lock() | 方法锁住m,如果 m 已经加锁,则阻塞直到 m 解锁。 |
(m *Mutex) Unlock() | 解锁 m,如果 m 未加锁会导致运行时错误。 |
2.2、简单的使用
sync.Mutex 是开箱即用的,就是声明了之后就可以使用
1 | package main |
互斥锁使用注意事项:
- 不要重复锁定互斥锁
- 不要忘记解锁互斥锁,必要时使用defer语句
- 不要对尚未锁定或者已解锁的互斥锁解锁
- 不要在多个函数之间直接传递互斥锁
2.3、数据结构
1 | type Mutex struct { |
state:代表互斥锁的状态
sema:互斥锁的信号量,mutex的阻塞队列的定位是通过这个变量来实现的,从而可以实现goroutine的阻塞和唤醒
互斥锁的状态
最低三位分别表示 mutexLocked
、mutexWoken
和 mutexStarving
,剩下的位置用来表示当前有多少个 Goroutine 在等待互斥锁的释放
默认情况下,互斥锁的所有状态位都是 0,int32
中的不同位分别表示了不同的状态:
mutexLocked — 表示互斥锁的锁定状态;
mutexWoken — 表示从正常模式被从唤醒;
mutexStarving — 当前的互斥锁进入饥饿状态;
waitersCount — 当前互斥锁上等待的 Goroutine 个数;
2.4、正常模式和饥饿模式
正常模式(非公平锁):锁的等待者会按照先进先出的顺序获取锁,唤醒的gotouine不会直接拥有锁,而是会和新请求竞争锁,因为新请求正在运行,获取被唤醒的goroutine大概率不会获取锁,一旦 Goroutine 超过 1ms 没有获取到锁,就会切换到饥饿模式
饥饿模式(公平锁):锁会直接给等待队列中的第一个goroutine,新的请求不会自旋也不会竞争锁,会直接放在等待队列的最后一位。 满足下面条件中的一个就会退出饥饿模式,G的执行时间小于1ms;等待队列清空
正常模式下的互斥锁能够提供更好地性能,饥饿模式的能避免 Goroutine 由于陷入等待无法获取锁而造成的高尾延时。
2.5、加锁
go/src/sync/mutex.go
1 | func (m *Mutex) Lock() { |
加锁过程
- 互斥锁的第1个阶段是使用原子操作快速抢占锁,如果抢占成功则 立即返回,如果抢占失败则调用lockSlow方法。
- 互斥锁的第2个阶段,使用信号量 进行同步。当信号量计 数值大于0时,意味着有其他协程执行了解锁操作,这时加锁协程可以 直接退出。当信号量计数值等于0时,意味着当前加锁协程需要陷入休 眠状态。
互斥锁自旋
自旋是一种多线程同步机制,当前的线程在进入自旋的过程中会一直保持 CPU 的占用,持续检查某个条件是否为真。在多核的 CPU 上,自旋可以避免 Goroutine 的切换,使用恰当会对性能带来很大的增益,但是使用的不恰当就会拖慢整个程序。
Goroutine 进入自旋的条件
- 互斥锁只有在普通模式才能进入自旋
- 运行在多 CPU 的机器上
- 当前 Goroutine 为了获取该锁进入自旋的次数小于四次
- 当前机器上至少存在一个正在运行的处理器 P 并且处理的运行队列为空