为 Go Mutex 实现 TryLock 方法
smallnest · · 2132 次点击 · · 开始浏览目录 [−]
Go标准库的sync/Mutex、RWMutex实现了sync/Locker接口, 提供了Lock()和UnLock()方法,可以获取锁和释放锁,我们可以方便的使用它来控制我们对共享资源的并发控制上。
但是标准库中的Mutex.Lock的锁被获取后,如果在未释放之前再调用Lock则会被阻塞住,这种设计在有些情况下可能不能满足我的需求。有时候我们想尝试获取锁,如果获取到了,没问题继续执行,如果获取不到,我们不想阻塞住,而是去调用其它的逻辑,这个时候我们就想要TryLock方法了。
虽然很早(13年)就有人给Go开发组提需求了,但是这个请求并没有纳入官方库中,最终在官方库的清理中被关闭了,也就是官方库目前不会添加这个方法。
顺便说一句, sync/Mutex的源代码实现可以访问这里,它应该是实现了一种自旋(spin)加休眠的方式实现, 有兴趣的读者可以阅读源码,或者阅读相关的文章,比如 Go Mutex 源码剖析。这不是本文要介绍的内容,读者可以找一些资料来阅读。
好了,转入正题,看看几种实现TryLock的方式吧。
使用 unsafe 操作指针
如果你查看sync/Mutex的代码,会发现Mutext的数据结构如下所示:
1234
type Mutex struct {state int32sema uint32}
它使用state这个32位的整数来标记锁的占用,所以我们可以使用CAS来尝试获取锁。
代码实现如下:
123456789
const mutexLocked = 1 << iotatype Mutex struct {sync.Mutex}func (m *Mutex) TryLock() bool {return atomic.CompareAndSwapInt32((*int32)(unsafe.Pointer(&m.Mutex)), 0, mutexLocked)}
使用起来和标准库的Mutex用法一样。
12345678910111213141516171819
func main() {var m Mutexm.Lock()go func() {m.Lock()}()time.Sleep(time.Second)fmt.Printf("TryLock: %t\n", m.TryLock()) //falsefmt.Printf("TryLock: %t\n", m.TryLock()) // falsem.Unlock()fmt.Printf("TryLock: %t\n", m.TryLock()) //truefmt.Printf("TryLock: %t\n", m.TryLock()) //falsem.Unlock()fmt.Printf("TryLock: %t\n", m.TryLock()) //truem.Unlock()}
注意TryLock不是检查锁的状态,而是尝试获取锁,所以TryLock返回true的时候事实上这个锁已经被获取了。
实现自旋锁
上面一节给了我们启发,利用 uint32和CAS操作我们可以一个自定义的锁:
1234567891011121314151617
type SpinLock struct {f uint32}func (sl *SpinLock) Lock() {for !sl.TryLock() {runtime.Gosched()}}func (sl *SpinLock) Unlock() {atomic.StoreUint32(&sl.f, 0)}func (sl *SpinLock) TryLock() bool {return atomic.CompareAndSwapUint32(&sl.f, 0, 1)}
整体来看,它好像是标准库的一个精简版,没有休眠和唤醒的功能。
当然这个自旋锁可以在大并发的情况下CPU的占用率可能比较高,这是因为它的Lock方法使用了自旋的方式,如果别人没有释放锁,这个循环会一直执行,速度可能更快但CPU占用率高。
当然这个版本还可以进一步的优化,尤其是在复制的时候。下面是一个优化的版本:
1234567891011121314151617181920
type spinLock uint32func (sl *spinLock) Lock() {for !atomic.CompareAndSwapUint32((*uint32)(sl), 0, 1) {runtime.Gosched() //without this it locks up on GOMAXPROCS > 1}}func (sl *spinLock) Unlock() {atomic.StoreUint32((*uint32)(sl), 0)}func (sl *spinLock) TryLock() bool {return atomic.CompareAndSwapUint32((*uint32)(sl), 0, 1)}func SpinLock() sync.Locker {var lock spinLockreturn &lock}
使用 channel 实现
另一种方式是使用channel:
12345678910111213141516171819202122232425
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 truedefault:}return false}
有兴趣的同学可以关注我的同事写的库 lrita/gosync。
性能比较
首先看看上面三种方式和标准库中的Mutex、RWMutex的Lock和Unlock的性能比较:
12345
BenchmarkMutex_LockUnlock-4 100000000 16.8 ns/op 0 B/op 0 allocs/opBenchmarkRWMutex_LockUnlock-4 50000000 36.8 ns/op 0 B/op 0 allocs/opBenchmarkUnsafeMutex_LockUnlock-4 100000000 16.8 ns/op 0 B/op 0 allocs/opBenchmarkChannMutex_LockUnlock-4 20000000 65.6 ns/op 0 B/op 0 allocs/opBenchmarkSpinLock_LockUnlock-4 100000000 18.6 ns/op 0 B/op 0 allocs/op
可以看到单线程(goroutine)的情况下`spinlock`并没有比标准库好多少,反而差一点,并发测试的情况比较好,如下表中显示,这是符合预期的。
unsafe方式和标准库差不多。
channel方式的性能就比较差了。
12345
BenchmarkMutex_LockUnlock_C-4 20000000 75.3 ns/op 0 B/op 0 allocs/opBenchmarkRWMutex_LockUnlock_C-4 20000000 100 ns/op 0 B/op 0 allocs/opBenchmarkUnsafeMutex_LockUnlock_C-4 20000000 75.3 ns/op 0 B/op 0 allocs/opBenchmarkChannMutex_LockUnlock_C-4 10000000 231 ns/op 0 B/op 0 allocs/opBenchmarkSpinLock_LockUnlock_C-4 50000000 32.3 ns/op 0 B/op 0 allocs/op
再看看三种实现TryLock方法的锁的性能:
123
BenchmarkUnsafeMutex_Trylock-4 50000000 34.0 ns/op 0 B/op 0 allocs/opBenchmarkChannMutex_Trylock-4 20000000 83.8 ns/op 0 B/op 0 allocs/opBenchmarkSpinLock_Trylock-4 50000000 30.9 ns/op 0 B/op 0 allocs/op
参考资料
本文参考了下面的文章和开源项目:
- https://github.com/golang/go/issues/6123
- https://github.com/LK4D4/trylock/blob/master/trylock.go
- https://github.com/OneOfOne/go-utils/blob/master/sync/spinlock.go
- http://codereview.stackexchange.com/questions/60332/is-my-spin-lock-implementation-correct
- https://github.com/lrita/gosync
有疑问加站长微信联系(非本文作者)
入群交流(和以上内容无关):加入Go大咖交流群,或添加微信:liuxiaoyan-s 备注:入群;或加QQ群:692541889
关注微信- 请尽量让自己的回复能够对别人有帮助
- 支持 Markdown 格式, **粗体**、~~删除线~~、
`单行代码` - 支持 @ 本站用户;支持表情(输入 : 提示),见 Emoji cheat sheet
- 图片支持拖拽、截图粘贴等方式上传
收入到我管理的专栏 新建专栏
目录 [−]
Go标准库的sync/Mutex、RWMutex实现了sync/Locker接口, 提供了Lock()和UnLock()方法,可以获取锁和释放锁,我们可以方便的使用它来控制我们对共享资源的并发控制上。
但是标准库中的Mutex.Lock的锁被获取后,如果在未释放之前再调用Lock则会被阻塞住,这种设计在有些情况下可能不能满足我的需求。有时候我们想尝试获取锁,如果获取到了,没问题继续执行,如果获取不到,我们不想阻塞住,而是去调用其它的逻辑,这个时候我们就想要TryLock方法了。
虽然很早(13年)就有人给Go开发组提需求了,但是这个请求并没有纳入官方库中,最终在官方库的清理中被关闭了,也就是官方库目前不会添加这个方法。
顺便说一句, sync/Mutex的源代码实现可以访问这里,它应该是实现了一种自旋(spin)加休眠的方式实现, 有兴趣的读者可以阅读源码,或者阅读相关的文章,比如 Go Mutex 源码剖析。这不是本文要介绍的内容,读者可以找一些资料来阅读。
好了,转入正题,看看几种实现TryLock的方式吧。
使用 unsafe 操作指针
如果你查看sync/Mutex的代码,会发现Mutext的数据结构如下所示:
1234
type Mutex struct {state int32sema uint32}
它使用state这个32位的整数来标记锁的占用,所以我们可以使用CAS来尝试获取锁。
代码实现如下:
123456789
const mutexLocked = 1 << iotatype Mutex struct {sync.Mutex}func (m *Mutex) TryLock() bool {return atomic.CompareAndSwapInt32((*int32)(unsafe.Pointer(&m.Mutex)), 0, mutexLocked)}
使用起来和标准库的Mutex用法一样。
12345678910111213141516171819
func main() {var m Mutexm.Lock()go func() {m.Lock()}()time.Sleep(time.Second)fmt.Printf("TryLock: %t\n", m.TryLock()) //falsefmt.Printf("TryLock: %t\n", m.TryLock()) // falsem.Unlock()fmt.Printf("TryLock: %t\n", m.TryLock()) //truefmt.Printf("TryLock: %t\n", m.TryLock()) //falsem.Unlock()fmt.Printf("TryLock: %t\n", m.TryLock()) //truem.Unlock()}
注意TryLock不是检查锁的状态,而是尝试获取锁,所以TryLock返回true的时候事实上这个锁已经被获取了。
实现自旋锁
上面一节给了我们启发,利用 uint32和CAS操作我们可以一个自定义的锁:
1234567891011121314151617
type SpinLock struct {f uint32}func (sl *SpinLock) Lock() {for !sl.TryLock() {runtime.Gosched()}}func (sl *SpinLock) Unlock() {atomic.StoreUint32(&sl.f, 0)}func (sl *SpinLock) TryLock() bool {return atomic.CompareAndSwapUint32(&sl.f, 0, 1)}
整体来看,它好像是标准库的一个精简版,没有休眠和唤醒的功能。
当然这个自旋锁可以在大并发的情况下CPU的占用率可能比较高,这是因为它的Lock方法使用了自旋的方式,如果别人没有释放锁,这个循环会一直执行,速度可能更快但CPU占用率高。
当然这个版本还可以进一步的优化,尤其是在复制的时候。下面是一个优化的版本:
1234567891011121314151617181920
type spinLock uint32func (sl *spinLock) Lock() {for !atomic.CompareAndSwapUint32((*uint32)(sl), 0, 1) {runtime.Gosched() //without this it locks up on GOMAXPROCS > 1}}func (sl *spinLock) Unlock() {atomic.StoreUint32((*uint32)(sl), 0)}func (sl *spinLock) TryLock() bool {return atomic.CompareAndSwapUint32((*uint32)(sl), 0, 1)}func SpinLock() sync.Locker {var lock spinLockreturn &lock}
使用 channel 实现
另一种方式是使用channel:
12345678910111213141516171819202122232425
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 truedefault:}return false}
有兴趣的同学可以关注我的同事写的库 lrita/gosync。
性能比较
首先看看上面三种方式和标准库中的Mutex、RWMutex的Lock和Unlock的性能比较:
12345
BenchmarkMutex_LockUnlock-4 100000000 16.8 ns/op 0 B/op 0 allocs/opBenchmarkRWMutex_LockUnlock-4 50000000 36.8 ns/op 0 B/op 0 allocs/opBenchmarkUnsafeMutex_LockUnlock-4 100000000 16.8 ns/op 0 B/op 0 allocs/opBenchmarkChannMutex_LockUnlock-4 20000000 65.6 ns/op 0 B/op 0 allocs/opBenchmarkSpinLock_LockUnlock-4 100000000 18.6 ns/op 0 B/op 0 allocs/op
可以看到单线程(goroutine)的情况下`spinlock`并没有比标准库好多少,反而差一点,并发测试的情况比较好,如下表中显示,这是符合预期的。
unsafe方式和标准库差不多。
channel方式的性能就比较差了。
12345
BenchmarkMutex_LockUnlock_C-4 20000000 75.3 ns/op 0 B/op 0 allocs/opBenchmarkRWMutex_LockUnlock_C-4 20000000 100 ns/op 0 B/op 0 allocs/opBenchmarkUnsafeMutex_LockUnlock_C-4 20000000 75.3 ns/op 0 B/op 0 allocs/opBenchmarkChannMutex_LockUnlock_C-4 10000000 231 ns/op 0 B/op 0 allocs/opBenchmarkSpinLock_LockUnlock_C-4 50000000 32.3 ns/op 0 B/op 0 allocs/op
再看看三种实现TryLock方法的锁的性能:
123
BenchmarkUnsafeMutex_Trylock-4 50000000 34.0 ns/op 0 B/op 0 allocs/opBenchmarkChannMutex_Trylock-4 20000000 83.8 ns/op 0 B/op 0 allocs/opBenchmarkSpinLock_Trylock-4 50000000 30.9 ns/op 0 B/op 0 allocs/op
参考资料
本文参考了下面的文章和开源项目: