Golang TryLock

Golang中的锁有sync包中的MutexRWMutex。然而Go中的锁实现的比较简单,在没有获取到锁的时候,会被阻塞住,一直等下去。 这样的锁的设计,有时候不容易满足现实中的需求,比如一个事务需要更新两个资源A和B, 我们在锁了A后需要锁B,然而恰好这个时候有人锁了B但是在等着锁A,这个时候Go的锁机制就会出现死锁了。 如果这个时候,我们不是用这种阻塞的锁,而是用一个名不付实的TryLock就可以解决这个问题了。

很多年以前,有人就给Go提了Issue,要求添加TryLocksync: mutex.TryLock。 然而不幸的是这个请求一直没有被纳入官方库,到最后在官方清理的时候还给关掉了,意味着官方库不会添加这个需求了。 这个Issue中,Rob Pike还顺便吐槽了一下TryLock是如何的名不付实,返回true的时候明明已经真的锁了,名字却叫做尝试。

Unsafe 直接操作 Mutex

Mutex的结构如下:

// A Mutex is a mutual exclusion lock.
// The zero value for a Mutex is an unlocked mutex.
//
// A Mutex must not be copied after first use.
type Mutex struct {
    state int32
    sema  uint32
}

mutexLocked = 1 << iota // mutex is locked

Mutex使用其中的state字段来标记锁是否被占用,0是unlock, 1是locked。 所以,我们可以取巧的来直接操作state,反正官方实现的quick path也是这样的。

const mutexLocked = 1 << iota  // mutex is locked

type Mutex struct {
    sync.Mutex
}

func (m *Mutex) TryLock() bool {
    return atomic.CompareAndSwapInt32((*int32)(unsafe.Pointer(&m.Mutex)), 0, mutexLocked)
}

无休眠版 SpinLock

利用CAS来尝试获取锁,还可以创建一个自己的spinlock

type SpinLock struct {
    state uint32
}

func (sl *SpinLock) Lock() {
    for !sl.TryLock() {
        runtime.Gosched()
    }
}

func (sl *SpinLock) Unlock() {
    atomic.StoreUint32(&sl.state, 0)
}

func (sl *SpinLock) TryLock() bool {
    return atomic.CompareAndSwapUint32(&sl.state, 0, mutexLocked)
}

看起来,就是一个标准库的Mutex的精简版,只有quick path,在quick path走不通的时候,没有slow path中的休眠唤醒等。 这样的SpinLock在并发量很大的时候,肯定是CPU占用很高的,因为如果锁被别人占用很长的时间,这个Spin的循环会一直跑。 获取锁的速度可能很快,但是CPU占用率会比较高。

这个版本还可以优化到不要结构

type spinLock uint32

func (sl *spinLock) Lock() {
    for !atomic.CompareAndSwapUint32((*uint32)(sl), 0, mutexLocked) {
        runtime.Gosched()
    }
}

func (sl *spinLock) Unlock() {
    atomic.StoreUint32((*uint32)(sl), 0)
}

func (sl *spinLock) TryLock() bool {
    return atomic.CompareAndSwapUint32((*uint32)(sl), 0, mutexLocked)
}

func SpinLock() sync.Locker {
    var lock spinLock
    return &lock
}

使用 channel 做锁

使用无buffer的channel也可以做把锁,但是需要注意的是,这个Unlock如果连续两次调用的话,会把自己锁住。

type ChanMutex chan struct{}

func (m *ChanMutex) Lock() {
    ch := (chan struct{})(*m)
    ch <- struct{}{}
}

func (m *ChanMutex) Unlock() {
    ch := (chan struct{})(*m)
    select {
    case <-ch:
    default:
        panic("unlock of unlocked mutex")
    }
}

func (m *ChanMutex) TryLock() bool {
    ch := (chan struct{})(*m)
    select {
    case ch <- struct{}{}:
        return true
    default:
    }
    return false
}

func NewLocker() *ChanMutex {
    l := make(ChanMutex, 1)
    return &l
}