go單例實現—雙重檢測是否安全

起因

今天看到項目中的kafka客戶端包裝結構體的獲取是單例模式<br>單例的實現是老生常談的問題了,懶漢餓漢線程安全,因爲看到項目中寫的還是有些問題,網上go單例實現的搜索結果比較少經測試也並不靠譜,所以在這記錄下

現狀

當前有的項目直接使用Mutex鎖,有的就直接判斷nil則創建,對於前者,每次都加鎖性能差,對於後者則會出現多個實例,也就不是單例了

改進

進而想要改進一下,在這不討論餓漢和線程非安全的實現,對於go中線程安全的懶漢實現,常見兩種:

  • 雙重檢驗
  • sync.Once

雙重檢驗示例:

 

package main

import (
    "sync"
    "testing"
)

var (
    instance *int
    lock      sync.Mutex
)

func getInstance() *int {
    if instance == nil {
        lock.Lock()
        defer lock.Unlock()

        if instance == nil {
            i := 1
            instance = &i
        }
    }
    return instance
}

// 用於下邊基準測試
func BenchmarkSprintf(b *testing.B){

    for i:=0;i<b.N;i++{
        go getInstance()
    }
}

是否線程安全

基於java中雙重檢驗鎖的經驗,因爲jvm的內存模型,雙重檢驗鎖會出現可見性問題,可以通過 volatile解決
那麼在go裏會有類似問題嗎?
關鍵點在於instance變量的讀和寫是否是原子操作
這裏做了個race競態檢測:

 

可以看到
20行的寫入和14行的讀取發生了競態
上例中用64位(系統是64位)的int指針表示一個實例,也說明了對於64位數據的寫入和讀取是非原子操作

我們看另一種實現:sync.Once方法

 

package main

import (
    "sync"
    "testing"
)

var (
    instance *int
    once      sync.Once
)

func getInstance() *int {
    once.Do(func(){
        if instance == nil {
            i := 1
            instance = &i
        }
    })
    return instance
}

func BenchmarkSprintf(b *testing.B){

    for i:=0;i<b.N;i++{
        go getInstance()
    }
}

實現比雙重檢驗看起來要整潔許多

race檢測結果:

 

沒有發生競態

關於sync.Once

那麼sync.Once是怎麼實現的呢

看下源碼:

 

package sync

import (
   "sync/atomic"
)

type Once struct {
   done uint32
   m    Mutex
}

func (o *Once) Do(f func()) {
   if atomic.LoadUint32(&o.done) == 0 {
      o.doSlow(f)
   }
}

func (o *Once) doSlow(f func()) {
   o.m.Lock()
   defer o.m.Unlock()
   if o.done == 0 {
      defer atomic.StoreUint32(&o.done, 1)
      f()
   }
}

可以看到sync.Once內部其實也是一個雙重檢驗鎖,但是對於共享變量(done字段)的讀和寫使用了atomic包的StoreUint32和LoadUint32方法

sync.Once使用一個32位無符號整數表示共享變量,即使是32位變量的讀寫操作都需要atomic包方法來實現原子性,更說明了go裏邊指針的讀寫不能保證原子性

關於atomic和metex

引用一段話:https://ms2008.github.io/2019/05/12/golang-data-race/

解決 race 的問題時,無非就是上鎖。可能很多人都聽說過一個高逼格的詞叫「無鎖隊列」。 都一聽到加鎖就覺得很 low,那無鎖又是怎麼一回事?其實就是利用 atomic 特性,那 atomic 會比 mutex 有什麼好處呢?go race detector 的作者總結了這兩者的一個區別:
Mutexes do no scale. Atomic loads do.
mutex 由操作系統實現,而 atomic 包中的原子操作則由底層硬件直接提供支持。在 CPU 實現的指令集裏,有一些指令被封裝進了 atomic 包,這些指令在執行的過程中是不允許中斷(interrupt)的,因此原子操作可以在 lock-free 的情況下保證併發安全,並且它的性能也能做到隨 CPU 個數的增多而線性擴展。
若實現相同的功能,後者通常會更有效率,並且更能利用計算機多核的優勢。所以,以後當我們想併發安全的更新一些變量的時候,我們應該優先選擇用 atomic 來實現。

結論

  • go單例實現—雙重檢測法對共享變量直接讀取和賦值是不安全的,需要atomic包實現原子操作的讀寫
  • 對於懶漢模式單例的實現,sync.Once是更好的辦法,簡潔安全,sync.Once已經幫我們實現了安全的雙重檢驗,能做到加載完成後不再加鎖
  • 這裏也提醒我們,只要是對於共享變量的併發訪問,一定要注意安全性,go更推崇避免共享變量,使用chan來交流信息,如果無法避免共享內存,優先使用atomic實現,其次sync,安全第一!
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章