在使用 Golang 做并发编程的过程中,是开发中必不可少的工具之一,它可以避免多协程对共享资源的并发读写,通过加锁来解决对共享资源的并发控制。

Lock()

在 Go 语言中提供了互斥锁sync.Mutex{}和读写锁sync.RWMutex{}。他们都实现了sync.Locker接口:

// A Locker represents an object that can be locked and unlocked.
type Locker interface {
    Lock()
    Unlock()
}

在通过调用Lock()获得锁之后,其他协程如果再次调用Lock()获取锁会被阻塞,直到上一个锁被解锁之后才能重新获得锁继续执行,其实这种锁模式就是我们常说的自旋锁,也就是循环加锁,在未获取到锁之前就会一直阻塞,直到加锁成功。

但是在现实的项目中,这种阻塞加锁并不适用于所有场景,有些场景我们并不需要这种阻塞,而我们期望的是在获取不到锁的情况下,直接返回,不要阻塞,由开发者去控制该进入何种逻辑。

TryLock()

在 Go 语言的早期版本中,并没类似的上述非阻塞的实现,但也不乏很多第三方的类似的实现。

但在 Go1.18 中,官方增加了这一特性:TryLock(),也算是给开发者多了一种选择。就是在原有sync.Mutex{}sync.RWMutex{}中增加了TryLock()方法:

// TryLock tries to lock m and reports whether it succeeded.
//
// See package [sync.Mutex] documentation.
func (m *Mutex) TryLock() bool {
    old := m.state
    if old&(mutexLocked|mutexStarving) != 0 {
        return false
    }

    // There may be a goroutine waiting for the mutex, but we are
    // running now and can try to grab the mutex before that
    // goroutine wakes up.
    if !atomic.CompareAndSwapInt32(&m.state, old, old|mutexLocked) {
        return false
    }

    if race.Enabled {
        race.Acquire(unsafe.Pointer(m))
    }
    return true
}

顾名思义,TryLock就是尝试去加锁,如果锁定成功返回true,锁定失败false

它获取锁的逻辑和Lock()基本一致,不同是,它不会因加锁失败而阻塞等锁释放,而是直接返回false

需要注意是,在TryLock()成功后,最终依然需要调用Unlock()解锁。

应用场景

Go 官方虽然加入了TryLock()方法,但并没有给出明确的应用场景。但在我看来也绝不是一无是处,不然也不会在众多开发者的期待中官方决定加入了这一特性。它适用一些非阻塞获得锁的场景,避免阻塞,实现轻量级的并发控制场景。

  • 无序的资源竞争,拿到就拿,拿不到就返回
  • 避免重复并发读写,只认先到的操作请求,在未处理完之前,直接拦截
  • 允许个别操作被丢弃,如果本次操作加锁失败,就跳过本次,等待下一次操作唤醒
  • 分布式锁的优雅降级,优先本地加锁,如果本地加锁失败则不再远程加锁

结合自己平时的工作,这里只是简单归纳了一下TryLock()的场景,不一定没用,也不一定非要用,找最适合自己的场景即可。

最后

无论是阻塞锁还是非阻塞锁,在使用时都要充分考虑、谨慎使用,避免死锁、协程泄露等风险。在 Go 语言中,并不一定是解决并发问题的首选项,结合 channel context select 等特性来解决并发问题,可能更符合 Go 的设计哲学。