目录 [−]
Go标准库的sync/Mutex
、RWMutex
实现了sync/Locker
接口, 提供了Lock()
和UnLock()
方法,可以获取锁和释放锁,我们可以方便的使用它来控制我们对共享资源的并发控制上。
但是标准库中的Mutex.Lock
的锁被获取后,如果在未释放之前再调用Lock
则会被阻塞住,这种设计在有些情况下可能不能满足我的需求。有时候我们想尝试获取锁,如果获取到了,没问题继续执行,如果获取不到,我们不想阻塞住,而是去调用其它的逻辑,这个时候我们就想要TryLock
方法了。
虽然很早(13年)就有人给Go开发组提需求了,但是这个请求并没有纳入官方库中,最终在官方库的清理中被关闭了,也就是官方库目前不会添加这个方法。
顺便说一句, sync/Mutex
的源代码实现可以访问这里,它应该是实现了一种自旋(spin)加休眠的方式实现, 有兴趣的读者可以阅读源码,或者阅读相关的文章,比如 Go Mutex 源码剖析。这不是本文要介绍的内容,读者可以找一些资料来阅读。
好了,转入正题,看看几种实现TryLock
的方式吧。
使用 unsafe
操作指针
如果你查看sync/Mutex
的代码,会发现Mutext
的数据结构如下所示:
1 2 3 4
| type Mutex struct { state int32 sema uint32 }
|
它使用state
这个32位的整数来标记锁的占用,所以我们可以使用CAS
来尝试获取锁。
代码实现如下:
1 2 3 4 5 6 7 8 9
| const mutexLocked = 1 << iota type Mutex struct { sync.Mutex } func (m *Mutex) TryLock() bool { return atomic.CompareAndSwapInt32((*int32)(unsafe.Pointer(&m.Mutex)), 0, mutexLocked) }
|
使用起来和标准库的Mutex
用法一样。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| func main() { var m Mutex m.Lock() go func() { m.Lock() }() time.Sleep(time.Second) fmt.Printf("TryLock: %t\n", m.TryLock()) fmt.Printf("TryLock: %t\n", m.TryLock()) m.Unlock() fmt.Printf("TryLock: %t\n", m.TryLock()) fmt.Printf("TryLock: %t\n", m.TryLock()) m.Unlock() fmt.Printf("TryLock: %t\n", m.TryLock()) m.Unlock() }
|
注意TryLock
不是检查锁的状态,而是尝试获取锁,所以TryLock
返回true的时候事实上这个锁已经被获取了。
实现自旋锁
上面一节给了我们启发,利用 uint32
和CAS
操作我们可以一个自定义的锁:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| 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占用率高。
当然这个版本还可以进一步的优化,尤其是在复制的时候。下面是一个优化的版本:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| type spinLock uint32 func (sl *spinLock) Lock() { for !atomic.CompareAndSwapUint32((*uint32)(sl), 0, 1) { runtime.Gosched() } } 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 spinLock return &lock }
|
使用 channel 实现
另一种方式是使用channel:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25
| 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 }
|
有兴趣的同学可以关注我的同事写的库 lrita/gosync。
性能比较
首先看看上面三种方式和标准库中的Mutex
、RWMutex
的Lock
和Unlock
的性能比较:
1 2 3 4 5
| BenchmarkMutex_LockUnlock-4 100000000 16.8 ns/op 0 B/op 0 allocs/op BenchmarkRWMutex_LockUnlock-4 50000000 36.8 ns/op 0 B/op 0 allocs/op BenchmarkUnsafeMutex_LockUnlock-4 100000000 16.8 ns/op 0 B/op 0 allocs/op BenchmarkChannMutex_LockUnlock-4 20000000 65.6 ns/op 0 B/op 0 allocs/op BenchmarkSpinLock_LockUnlock-4 100000000 18.6 ns/op 0 B/op 0 allocs/op
|
可以看到单线程(goroutine)的情况下`spinlock`并没有比标准库好多少,反而差一点,并发测试的情况比较好,如下表中显示,这是符合预期的。
unsafe
方式和标准库差不多。
channel
方式的性能就比较差了。
1 2 3 4 5
| BenchmarkMutex_LockUnlock_C-4 20000000 75.3 ns/op 0 B/op 0 allocs/op BenchmarkRWMutex_LockUnlock_C-4 20000000 100 ns/op 0 B/op 0 allocs/op BenchmarkUnsafeMutex_LockUnlock_C-4 20000000 75.3 ns/op 0 B/op 0 allocs/op BenchmarkChannMutex_LockUnlock_C-4 10000000 231 ns/op 0 B/op 0 allocs/op BenchmarkSpinLock_LockUnlock_C-4 50000000 32.3 ns/op 0 B/op 0 allocs/op
|
再看看三种实现TryLock
方法的锁的性能:
1 2 3
| BenchmarkUnsafeMutex_Trylock-4 50000000 34.0 ns/op 0 B/op 0 allocs/op BenchmarkChannMutex_Trylock-4 20000000 83.8 ns/op 0 B/op 0 allocs/op BenchmarkSpinLock_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