同步原语的适用场景
- 共享资源。并发地读写共享资源,会出现数据竞争的问题,所以需要Mutex、RWMutex这样的并发原语来保护
- 任务编排。需要goroutine按照一定的规律执行,而goroutine之间有相互等待或者依赖的顺序关系,常常使用WaitGroup或者Channel来实现
- 消息传递。信息交流以及不同的goroutine之间的线程安全的数据交流,常常使用Channel来实现
- 使用案例
// Counter 线程安全的计数器类型
type Counter struct {
mu sync.Mutex
count uint64
}
// incr 加1 的方法,内部使用互斥锁保护
func (c *Counter) incr() {
c.mu.Lock()
c.count++
c.mu.Unlock()
}
// Count 得到计数器的值,也需要锁保护
func (c *Counter) Count() uint64 {
c.mu.Lock()
defer c.mu.Unlock()
return c.count
}
第一节:mutex:解决资源并发访问问题
- 使用
race detector检测并发访问共享资源是否有问题,Google基于C/C++的sanitizers技术实现 ,编译器通过探测所有的内存访问,加入代码能监视对这些内存地址的访问(读/写),在运行代码的时候,此工具能监控到对共享变量的非同步访问,出现race的时候,就会打印出警告信息 - 可以在编译、测试或者运行的时候加入race参数:
go run -race main.go - 缺点:无法在编译时检查出race问题,在运行时加入指令才能检测出来
- Mutex的零值是还没有goroutine等待的未加锁的状态,所以不需要额外的初始化
第二节:mutex:庖丁解牛看实现
- 第一个阶段:先来先得
- 请求锁的goroutine会排队等候获取互斥锁。虽然貌似公平,但是从性能上来说,并不是最优的,因为如果能够把锁交给正在占用CPU时间片的goroutine的话,就不需要做上下文的额切换
-
第二个阶段:给新人机会
- 新来的goroutine也有机会先获得锁,甚至一个goroutine可能连续获取到锁,打破了先来先得的逻辑
- 请求锁的goroutine有两类,一类是新来得请求锁的goroutine,另一类是被唤醒的等待请求锁的goroutine
-
第三个阶段:多给些机会
- 如果新来的goroutine或者是被唤醒的goroutine首次获取不到锁,就会通过自旋,尝试检查锁是否被释放。在尝试一定自旋次数后,再执行原来的逻辑
- 对于临界区代码执行非常短的场景来说,这是一个非常好的优化。因为临界区的代码耗时很短,锁很快就能释放,而抢夺锁的goroutine不用通过休眠唤醒方式等待调度,直接spin几次,可能就获得了锁
-
第四个阶段:解决饥饿(等待中的goroutine可能会一直获取不到锁)
- 加入饥饿模式,可以避免把机会全部留给新来的goroutine,保证请求锁的goroutine获取锁的公平性;mutex绝不容忍一个goroutine被落下,永远没有机会获取锁,尽可能让等待较长的goroutine更有机会获取到锁;
- 新来得goroutine参与竞争,有可能每次都会被新来的goroutine抢到获取锁的机会,在极端情况下,等待中的goroutine可能会一直获取不到锁,这就是饥饿问题
- 增加了饥饿模式,将饥饿模式的最大等待时间阈值设置成了1毫秒,意味着一旦等待者等待的时间超过了这个阈值,mutex的处理就可能进入饥饿模式,优先让等待者先获取到锁
- 通过加入饥饿模式,可以避免把机会全部留给新来的goroutine,保证了请求锁的goroutine获取锁公平性
- 正常模式下,waiter都是进入先进先出队列,被唤醒的waiter并不会直接持有锁,而是要和新来的goroutine进行竞争。新来的goroutine有先天的优势,他们正在CPU中运行,可能数量还不少,所以,在高并发情况下,被唤醒的waiter可能比较悲剧的获取不到锁,这是会被插入到队列的前面。如果waiter获取不到锁的时间超过阈值1ms,那么mutex就进入了饥饿模式
- 饥饿模式下,mutex的拥有者将直接把锁交给队列最前面的waiter。新来的goroutine不会尝试获取锁,即使看起来锁没有被持有,他也不会去抢,也不会spin,而是加入等待队列的尾部
- 转入正常模式:
- 此waiter已经是队列中的最后一个waiter了,没有其他的等待锁的goroutine了
- 此waiter的等待时间小于1ms
-
-
-
主要是正常模式(性能更好)+饥饿模式(是一种公平性和性能的一种平衡,优先对待的是那些一直在等待的waiter)
-
如果新来的goroutine或者是被唤醒的
第三节:mutex:4种易错场景大盘点
- Lock/Unlock不是成对出现
- Copy已使用的mutex
- Package sync的同步原语在使用后是不能复制的
- mutex是一个有状态的对象,他的state字段记录这个锁的状态
- 使用vet工具检测是否出现复制的
- 重入
- 不支持可重入锁(递归锁)
- 可重入锁(递归锁)解决了代码冲入或者递归调用带来的死锁问题,同时也可以要求只有持有锁的goroutine才能unlock这个锁
- 实现可重入锁
-
获取goroutine id
package main import ( "fmt" "github.com/petermattis/goid" "sync" "sync/atomic" ) type RecursiveMutex struct { sync.Mutex owner int64 //记录当前锁的拥有者goroutine的id recursion int32 //辅助字段,记录重入的次数 } func (m *RecursiveMutex) Lock() { gid := goid.Get() //如果当前持有锁的goroutine就是这次调用的goroutine,说明是重入 if atomic.LoadInt64(&m.owner) == gid { m.recursion++ return } m.Mutex.Lock() // 获得锁的goroutine第一次调用,记录下他的goroutine id,调用次数加1 // 原子操作的存储过程,将gid赋值给owner atomic.StoreInt64(&m.owner, gid) m.recursion = 1 } func (m *RecursiveMutex) Unlock() { gid := goid.Get() //非持有锁的goroutine尝试释放锁,错误的使用 if atomic.LoadInt64(&m.owner) != gid { panic(fmt.Sprintf("wrong the owner(%d):%d!", m.owner, gid)) } //调用次数减1 m.recursion-- if m.recursion != 0 { //如果这个goroutine还没有完全释放,则直接返回 return } // 此goroutine最后一次调用,需要释放锁 atomic.StoreInt64(&m.owner, -1) m.Mutex.Unlock() } -
token
/* - Go开发者不希望利用goroutine id做一些不确定的东西,所以没有暴露获取goroutine id的方法 - 调用者自己提供一个token,获取所得时候把这个token传入,释放所得时候也需要把这个token传入,通过用户传入的token替换方案1中goroutine id,其他逻辑和方案一致 */ import ( "sync" "sync/atomic" ) type TokenRecursiveMutex struct { sync.Mutex token int64 recursion int32 } func (m *TokenRecursiveMutex) Lock(token int64) { if atomic.LoadInt64(&m.token) == token { m.recursion++ return } m.Mutex.Lock() //传入的token不一致,说明不是递归调用 //抢到锁之后记录这个token atomic.StoreInt64(&m.token, token) m.recursion = 1 } func (m *TokenRecursiveMutex) Unlock(token int64) { if atomic.LoadInt64(&m.token) != token { panic(fmt.Sprintf("wrong the owner(%d):%d!", m.token, token)) } m.recursion-- //当前持有这个锁的token释放锁 if m.recursion != 0 { //如果这个goroutine还没有完全释放,则直接返回 return } // 此goroutine最后一次调用,需要释放锁 atomic.StoreInt64(&m.token, 0) m.Mutex.Unlock() }
-
- 死锁
第四节:mutex:如何拓展额外功能
- 如果互斥锁被某个goroutine获取了, 而且没有被释放,阿么其他请求这把锁的goroutine就会阻塞等待
- 有些情况下,获取不到锁并不需要一直等待,一直等待会导致业务处理能力下降
- 锁是性能下降的“罪魁祸首”之一,有效的降低锁的竞争,就能够很好的提高性能。
- TryLock
- 尝试获取锁,当一个goroutine调用这个TryLock方法请求锁的时候,如果这把锁没有被其他goroutine持有,这个goroutine就持有了这把锁,并返回true
- 如果这把锁已经被其他goroutine持有,或者是正在准备交给某个唤醒的goroutine,那么请求锁的goroutine就直接返回false,不会阻塞在方法调用上
- 获取当前等待mutex的goroutine的数量
- 查看是否饥饿、被持有、唤醒、线程安全的队列
import (
"fmt"
"sync"
"sync/atomic"
"time"
"unsafe"
)
const (
mutexLocked = 1 << iota //加锁标识位置
mutexWoken //唤醒标识位置
mutexStaving //锁饥饿标识位置
mutexWaiterShift = iota //标识waiter的起始bit位置
)
type Mutex struct {
sync.Mutex
}
// TryLock 尝试获取锁
func (m *Mutex) TryLock() bool {
//如果能成功抢到锁
if atomic.CompareAndSwapInt32((*int32)(unsafe.Pointer(&m.Mutex)), 0, mutexLocked) {
return true
}
// 如果处于唤醒、加锁或者饥饿状态,这次请求就不参与竞争了,返回false
old := atomic.LoadInt32((*int32)(unsafe.Pointer(&m.Mutex)))
if old&(mutexLocked|mutexStaving|mutexWoken) != 0 {
return false
}
new := old | mutexLocked
return atomic.CompareAndSwapInt32((*int32)(unsafe.Pointer(&m.Mutex)), old, new)
}
// Count 统计数量
func (m *Mutex) Count() int {
v := atomic.LoadInt32((*int32)(unsafe.Pointer(&m.Mutex)))
// 先右移3位
v = v>>mutexWaiterShift + (v & mutexLocked)
return int(v)
}
// IsLocked 锁是否被持有
func (m *Mutex) IsLocked() bool {
state := atomic.LoadInt32((*int32)(unsafe.Pointer(&m.Mutex)))
return state&mutexLocked == mutexLocked
}
// IsWoken 是否有等待者被唤醒
func (m *Mutex) IsWoken() bool {
state := atomic.LoadInt32((*int32)(unsafe.Pointer(&m.Mutex)))
//位与运算
return state&mutexWoken == mutexWoken
}
// IsStarving 锁是否处于饥饿状态
func (m *Mutex) IsStarving() bool {
state := atomic.LoadInt32((*int32)(unsafe.Pointer(&m.Mutex)))
return state&mutexStaving == mutexStaving
}
// 线程安全的队列
type SliceQueue struct {
mu sync.Mutex
data []interface{}
}
func NewSliceQueue(n int) (q *SliceQueue) {
return &SliceQueue{
mu: sync.Mutex{},
data: make([]interface{}, 0, n),
}
}
// Enqueue 添加到队尾
func (q *SliceQueue) Enqueue(v interface{}) {
q.mu.Lock()
q.data = append(q.data, v)
q.mu.Unlock()
}
// Dequeue 移除队头并返回
func (q *SliceQueue) Dequeue() interface{} {
q.mu.Lock()
if len(q.data) == 0 {
q.mu.Unlock()
return nil
}
v := q.data[0]
q.data = q.data[1:]
q.mu.Unlock()
return v
}