在使用 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 的设计哲学。