天天看點

go語言模拟實作單利模式

作者:幹飯人小羽
go語言模拟實作單利模式

最近幾年go語言的增長速度非常驚人,吸引着各界人士切換到Go語言。最近有很多關于使用Ruby語言的公司切換到Go、體驗Go語言、和Go的并行和并發解決問題的文章。

  過去10年裡,Ruby on Rails已經讓衆多的開發者和初創公司快速開發出強大的系統,大多數時候不需要擔心他的内部是如何工作的,或者擔心線程安全和并發。RoR程式很少建立線程和并行的運作一些東西。整個托管的基礎建設和架構棧使用不同的方法,通過多個程序來進行并行。最近幾年,像Puma這樣的多線程機架式伺服器開始流行,但是即使是這樣,剛開始也帶來了很多關于使用第三方gems和其他沒有被設計為線程安全的代碼的問題。

現在有很多開發者開始使用Go語言。我們需要仔細研究我們的代碼,并觀察代碼的行為,需要以線程安全的方式代碼設代碼。

常識性錯誤

   最近,我在很多Github庫裡看到這種類型的錯誤,單例模式的實作沒有考慮線程安全,下面是常識性錯誤的代碼

package singleton

type singleton struct {
}

var instance *singleton

func GetInstance() *singleton {
    if instance == nil {
        instance = &singleton{}   // <---非線程安全的
    }
    return instance
}           

  上面的示例中,多個go routines 會進行第一次檢查并且都會建立 singleton類型的執行個體并且互相覆寫。不能保證哪一個執行個體會被傳回,在這個執行個體上更進一步的操作可能和開發者所期望的不一至。

  這樣是有問題的,因為如果對這個單例的執行個體已經在代碼中被應用,可能會有潛在的多個這個類型的執行個體,并用有各自的狀态,産生潛在的不同的代碼行為。他也可能成為高度時的惡夢,并且很難定位錯誤,因為在debug時由于運作時暫停減少潛在的非線程安全的執行而不會真正出現錯誤,很容易隐藏開發者的問題。

激進的鎖

  我也看到一些使用糟糕的方法來解決線程安全的問題。事實上他解決了多線程的問題,但是創造了其他潛在的更嚴重的問題,他通過對整個方法執行鎖定來引入線程競争

var mu Sync.Mutex

func GetInstance() *singleton {
    mu.Lock()                    // <--- 如果執行個體已經被建立就沒有必要鎖寫
    defer mu.Unlock()

    if instance == nil {
        instance = &singleton{}
    }
    return instance
}           

  上面的代碼,我們可以看到,通過引入Sync.Mutex來解決線程安全的問題,并且在建立單例執行個體前擷取鎖。問題在于當我們不需要的時候例如,執行個體已經被建立的時候,隻需要傳回緩存的單例執行個體,但是呢也會執行鎖操作。在高并發代碼基礎上,這會産生瓶頸,因為在同一時間隻有一個go routine可以得到單例的執行個體。

是以這不是最好的方法,我們找找其他的解決方案。

Check-Lock-Check 模式

   在c++和其他語言,用于保證最小鎖定并且保證線程安全的最好、最安全的方式是當需要鎖定時使用衆所周知的Check-Lock-Check模式。下面的僞代碼說明了這個模式的大概樣子

if check() {
    lock() {
        if check() {
            // perform your lock-safe code here
        }
    }
}           

  這個模式背後的想法是想一開始就檢查。用于減少任何激進的鎖定。因為一個IF語句比鎖定便宜的多。第二我們想等待并擷取排他鎖是以的塊内同一時間隻能有一個執行,但是在第一次檢察和和排他鎖擷取之間可能會有其他線程想要擷取鎖,是以我們需要在塊内再次的檢查以避免單例執行個體被其他執行個體替換。

多年來,和我一起工作人的熟知這一點,在代碼審過程中,這個模式和線程安全思想方面,我對團隊非常嚴厲。

如果我們應用這個模式到我的GetInstance()方法,我們需要做的如下 :

func GetInstance() *singleton {
    if instance == nil {     // <-- 不夠完善. 他并不是完全的原子性
        mu.Lock()
        defer mu.Unlock()

        if instance == nil {
            instance = &singleton{}
        }
    }
    return instance
}           

  這是一個挺好的方法,但是并不完美。因為編譯器優化,但是沒有執行個體儲存的狀态的原子性檢查。全面的技術考慮,這并不是安美的。但是已經比之前的方法好多了。

  但是使用 sync/atomic 包,我們可以原子性的加載和設定辨別訓示是否已經初始化了我們的執行個體。

import "sync"
import "sync/atomic"

var initialized uint32
...

func GetInstance() *singleton {

    if atomic.LoadUInt32(&initialized) == 1 {
        return instance
    }

    mu.Lock()
    defer mu.Unlock()

    if initialized == 0 {
         instance = &singleton{}
         atomic.StoreUint32(&initialized, 1)
    }

    return instance
}           

  但是.....我相信我們可以通過檢視Go語言和标準庫的源碼看一下go routines 同步的實作方式來做的更好

Go慣用的單例方法

   我們想要使用Go的慣用手法來實作這個單例模式。是以我們需要看一下打包好的sync标準庫。我們找到了 Once 類型。這個對象可以精确的隻執行一次操作,下面就是Go标準庫的代碼

// Once is an object that will perform exactly one action.
type Once struct {
    m    Mutex
    done uint32
}

// Do calls the function f if and only if Do is being called for the
// first time for this instance of Once. In other words, given
//     var once Once
// if once.Do(f) is called multiple times, only the first call will invoke f,
// even if f has a different value in each invocation.  A new instance of
// Once is required for each function to execute.
//
// Do is intended for initialization that must be run exactly once.  Since f
// is niladic, it may be necessary to use a function literal to capture the
// arguments to a function to be invoked by Do:
//     config.once.Do(func() { config.init(filename) })
//
// Because no call to Do returns until the one call to f returns, if f causes
// Do to be called, it will deadlock.
//
// If f panics, Do considers it to have returned; future calls of Do return
// without calling f.
//
func (o *Once) Do(f func()) {
    if atomic.LoadUint32(&o.done) == 1 { // <-- Check
        return
    }
    // Slow-path.
    o.m.Lock()                           // <-- Lock
    defer o.m.Unlock()
    if o.done == 0 {                     // <-- Check
        defer atomic.StoreUint32(&o.done, 1)
        f()
    }
}           

  這意味着我們可以運用非常棒的 Go sync包來調用一個隻執行一次的方法。是以,我們可以向下面這樣調用 once.Do() 方法

once.Do(func() {
    // 執行安全的初始化操作
})           

  下面你可以看到使用sync.Once類型實作的單例實作的完整代碼,用于同步通路GetInstance() 并保證我們的類型初始化隻執行一次。

go語言模拟實作單利模式
package singleton

import (
    "sync"
)

type singleton struct {
}

var instance *singleton
var once sync.Once

func GetInstance() *singleton {
    once.Do(func() {
        instance = &singleton{}
    })
    return instance
}           

  是以,使用sync.Once包是一個完美的安全的實作方式,這種方式有點像Object-C和Swift(Cocoa)的實作dispatch_once 方法用于執行類似的初始化操作。

總結

當涉及到并行和并發代碼時,需要詳細檢查你的代碼。始終讓你的團隊成員進行代碼審查,是以對于這樣的事情才能更容易的監督。

所有的切換到Go語言的新開發者,需要明确的了解線程安全的原理才能更好的改善你的代碼。即使Go語言本身通過做了很多努力允許你使用很少的并發知識來設計并發代碼。仍有一些語言無法幫你處理的一些情況,你依然需要在開發代碼時應用最佳的實踐方法

繼續閱讀