互斥锁堵塞带来的性能下降
我们知道互斥锁如果被某个 goroutine 获取了,而且还没有释放,其他请求这把锁的 goroutine,就会阻塞等待,直到有机会获得这把锁。
有时候阻塞并不是一个好的选择,比如你请求锁更新一个计数器,如果获取不到锁的话没必要等待,大不了这次不更新,我下次更新就好了,如果阻塞的话会导致业务处理能力的下降。
TryLock - 尝试获取排外锁
为了解决上述问题,我们可以增加一个TryLock方法,实现非阻塞的尝试获取锁方法,
TryLock工作流程
- goroutine 调用TryLock 方法请求锁。
- 如果这把锁没有被其他 goroutine 所持有,goroutine 加锁成功,返回 true;
- 如果这把锁已经被其他 goroutine 所持有,或者是正在准备交给某个被唤醒的 goroutine,这个请求锁的 goroutine 就直接返回 false,不会阻塞在方法调用上。
实现
TryLock方法
package main
import (
"fmt"
"math/rand"
"sync"
"sync/atomic"
"time"
"unsafe"
)
// 复制Mutex定义的常量
const (
mutexLocked = 1 << iota // 加锁标识位置
mutexWoken // 唤醒标识位置
mutexStarving // 锁饥饿标识位置
mutexWaiterShift = iota // 标识waiter的起始bit位置
)
// Mutex 扩展一个Mutex结构
type Mutex struct {
sync.Mutex
}
// TryLock 尝试获取锁
func (m *Mutex) TryLock() bool {
// 如果能成功抢到锁
//fast path
if atomic.CompareAndSwapInt32((*int32)(unsafe.Pointer(&m.Mutex)), 0, mutexLocked) {
return true
}
// 如果处于唤醒、加锁或者饥饿状态,这次请求就不参与竞争了,返回false
old := atomic.LoadInt32((*int32)(unsafe.Pointer(&m.Mutex)))
if old&(mutexLocked|mutexStarving|mutexWoken) != 0 {
return false
}
// 尝试在竞争的状态下请求锁
new := old | mutexLocked
return atomic.CompareAndSwapInt32((*int32)(unsafe.Pointer(&m.Mutex)), old, new)
}
测试
定义一个协程持有锁,锁将会在随机时间内释放,另一个协程尝试获取锁,如果持有锁的协程在1s内释放,则获取成功,否则获取失败。
func try() {
var mu Mutex
go func() { // 启动一个goroutine持有一段时间的锁
mu.Lock()
time.Sleep(time.Duration(rand.Intn(2)) * time.Second)
mu.Unlock()
}()
time.Sleep(time.Second)
ok := mu.TryLock() // 尝试获取到锁
if ok { // 获取成功
fmt.Println("got the lock")
// do something
mu.Unlock()
return
}
// 没有获取到
fmt.Println("can't get the lock, return directly!")
}
func main() {
try()
}
实际场景应用
当我们需要并发的去修改配置文件时,我们想避免有多个额Gorotgine修改文件。
当某个 goroutine 想要更改配置数据时,如果发现已经有 goroutine 在更改了,其他的 goroutine 调用 TryLock,返回了 false,这个 goroutine 就会放弃更改。
如何监控Waiter的数量
我们前面说到锁竞争程度是跟性能成反比的,所以监控关键互斥锁上等待的 goroutine 的数量,是我们分析锁竞争的激烈程度的一个重要指标。
Muntex结构
Mutex包含两个字段,state 和 sema。前四个字节(int32)就是 state 字段,Mutex 结构中的 state 字段有很多个含义,通过 state 字段,你可以知道锁是否已经被某个 goroutine 持有、当前是否处于饥饿状态、是否有等待的 goroutine 被唤醒、等待者的数量等信息,我们可以通过unsafe的方法获取到这些信息。
type Mutex struct {
state int32
sema uint32
}
Count实现
const (
mutexLocked = 1 << iota // mutex is locked
mutexWoken
mutexStarving
mutexWaiterShift = iota
)
type Mutex struct {
sync.Mutex
}
func (m *Mutex) Count() int {
// 获取state字段的值
v := atomic.LoadInt32((*int32)(unsafe.Pointer(&m.Mutex)))
//右移三位(这里的常量 mutexWaiterShift 的值为 3)得到了当前等待者的数量
//如果当前的锁已经被其他 goroutine 持有,加1
v = v>>mutexWaiterShift + (v & mutexLocked)
return int(v)
}
锁的状态判断
state 这个字段的第一位是用来标记锁是否被持有,第二位用来标记是否已经唤醒了一个等待者,第三位标记锁是否处于饥饿状态。
实现
const (
mutexLocked = 1 << iota // mutex is locked
mutexWoken
mutexStarving
mutexWaiterShift = iota
)
// 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&mutexStarving == mutexStarving
}
测试
func PrintCount() {
var mu Mutex
for i := 0; i < 1000; i++ { // 启动1000个goroutine
go func() {
mu.Lock()
time.Sleep(time.Second)
mu.Unlock()
}()
}
time.Sleep(time.Second)
// 输出锁的信息
fmt.Printf("waitings: %d, isLocked: %t, woken: %t, starving: %t\n", mu.Count(), mu.IsLocked(), mu.IsWoken(), mu.IsStarving())
}
func main() {
PrintCount()
}
result:
waitings: 999, isLocked: true, woken: false, starving: false
实现一个线程安全的队列
Go的标准库中没有线程安全的队列数据结构的实现,我们可以通过 Mutex 实现一个简单的队列。通过 Mutex 我们可以为一个非线程安全的 data interface{}实现线程安全的访问。
实现
package main
import "sync"
type SliceQueue struct {
data []interface{}
mu sync.Mutex
}
func NewSliceQueue(n int) (q *SliceQueue) {
return &SliceQueue{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
}