這是一個建立於 的文章,其中的資訊可能已經有所發展或是發生改變。
目錄 [−]
- 使用 unsafe 操作指標
- 實現自旋鎖
- 使用 channel 實現
- 效能比較
- 參考資料
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