為 Go Mutex 實現 TryLock 方法

來源:互聯網
上載者:User
這是一個建立於 的文章,其中的資訊可能已經有所發展或是發生改變。

目錄 [−]

  1. 使用 unsafe 操作指標
  2. 實現自旋鎖
  3. 使用 channel 實現
  4. 效能比較
  5. 參考資料

Go標準庫的sync/MutexRWMutex實現了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的時候事實上這個鎖已經被擷取了。

實現自旋鎖

上面一節給了我們啟發,利用 uint32CAS操作我們可以一個自訂的鎖:

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。

效能比較

首先看看上面三種方式和標準庫中的MutexRWMutexLockUnlock的效能比較:

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

參考資料

本文參考了下面的文章和開源項目:

  1. https://github.com/golang/go/issues/6123
  2. https://github.com/LK4D4/trylock/blob/master/trylock.go
  3. https://github.com/OneOfOne/go-utils/blob/master/sync/spinlock.go
  4. http://codereview.stackexchange.com/questions/60332/is-my-spin-lock-implementation-correct
  5. https://github.com/lrita/gosync
相關文章

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.