在過去的幾年中,Go語言的發展是驚人的,并且吸引了很多由其他語言(Python、PHP、Ruby)轉向Go語言的跨語言學習者, Go語言太容易實作并發了,以至于它在很多地方被不正確的使用了,
Go語言中的單例模式
在過去的幾年中,Go語言的發展是驚人的,并且吸引了很多由其他語言(Python、PHP、Ruby)轉向Go語言的跨語言學習者,
在過去的很長時間里,很多開發人員和初創公司都習慣使用Python、PHP或Ruby快速開發功能強大的系統,并且大多數情況下都不需要擔心內部事務如何作業,也不需要擔心執行緒安全性和并發性,直到最近幾年,多執行緒高并發的系統開始流行起來,我們現在不僅需要快速開發功能強大的系統,而且還要保證被開發的系統能夠足夠快速運行,(我們真是太難了??)
對于被Go語言天生支持并發的特性吸引來的跨語言學習者來說,我覺著掌握Go語言的語法并不是最難的,最難的是突破既有的思維定勢,真正理解并發和使用并發來解決實際問題,
Go語言太容易實作并發了,以至于它在很多地方被不正確的使用了,
常見的錯誤
有一些錯誤是很常見的,比如不考慮并發安全的單例模式,就像下面的示例代碼:
package singleton
type singleton struct {}
var instance *singleton
func GetInstance() *singleton {
if instance == nil {
instance = &singleton{} // 不是并發安全的
}
return instance
}
在上述情況下,多個goroutine可以執行第一個檢查,并且它們都將創建該singleton型別的實體并相互覆寫,無法保證它將在此處回傳哪個實體,并且對該實體的其他進一步操作可能與開發人員的期望不一致,
不好的原因是,如果有代碼保留了對該單例實體的參考,則可能存在具有不同狀態的該型別的多個實體,從而產生潛在的不同代碼行為,這也成為除錯程序中的一個噩夢,并且很難發現該錯誤,因為在除錯時,由于運行時暫停而沒有出現任何錯誤,這使非并發安全執行的可能性降到了最低,并且很容易隱藏開發人員的問題,
激進的加鎖
也有很多對這種并發安全問題的糟糕解決方案,使用下面的代碼確實能解決并發安全問題,但會帶來其他潛在的嚴重問題,通過加鎖把對該函式的并發呼叫變成了串行,
var mu Sync.Mutex
func GetInstance() *singleton {
mu.Lock() // 如果實體存在沒有必要加鎖
defer mu.Unlock()
if instance == nil {
instance = &singleton{}
}
return instance
}
在上面的代碼中,我們可以看到在創建單例實體之前通過引入Sync.Mutex和獲取Lock來解決并發安全問題,問題是我們在這里執行了過多的鎖定,即使我們不需要這樣做,在實體已經創建的情況下,我們應該簡單地回傳快取的單例實體,在高度并發的代碼基礎上,這可能會產生瓶頸,因為一次只有一個goroutine可以獲得單例實體,
因此,這不是最佳方法,我們必須考慮其他解決方案,
Check-Lock-Check模式
在C ++和其他語言中,確保最小程度的鎖定并且仍然是并發安全的最佳和最安全的方法是在獲取鎖定時利用眾所周知的Check-Lock-Check模式,該模式的偽代碼表示如下,
if check() {
lock() {
if check() {
// 在這里執行加鎖安全的代碼
}
}
}
該模式背后的思想是,你應該首先進行檢查,以最小化任何主動鎖定,因為IF陳述句的開銷要比加鎖小,其次,我們希望等待并獲取互斥鎖,這樣在同一時刻在那個塊中只有一個執行,但是,在第一次檢查和獲取互斥鎖之間,可能有其他goroutine獲取了鎖,因此,我們需要在鎖的內部再次進行檢查,以避免用另一個實體覆寫了實體,
如果將這種模式應用于我們的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語言和標準庫如何實作goroutine同步來做得更好,
Go語言慣用的單例模式
我們希望利用Go慣用的方式來實作這個單例模式,我們在標準庫sync中找到了Once型別,它能保證某個操作僅且只執行一次,下面是來自Go標準庫的原始碼(部分注釋有刪改),
// Once is an object that will perform exactly one action.
type Once struct {
// done indicates whether the action has been performed.
// It is first in the struct because it is used in the hot path.
// The hot path is inlined at every call site.
// Placing done first allows more compact instructions on some architectures (amd64/x86),
// and fewer instructions (to calculate offset) on other architectures.
done uint32
m Mutex
}
func (o *Once) Do(f func()) {
if atomic.LoadUint32(&o.done) == 0 { // check
// Outlined slow-path to allow inlining of the fast-path.
o.doSlow(f)
}
}
func (o *Once) doSlow(f func()) {
o.m.Lock() // lock
defer o.m.Unlock()
if o.done == 0 { // check
defer atomic.StoreUint32(&o.done, 1)
f()
}
}
這說明我們可以借助這個實作只執行一次某個函式/方法,once.Do()的用法如下:
once.Do(func() {
// 在這里執行安全的初始化
})
下面就是單例實作的完整代碼,該實作利用sync.Once型別去同步對GetInstance()的訪問,并確保我們的型別僅被初始化一次,
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包是安全地實作此目標的首選方式,類似于Objective-C和Swift(Cocoa)實作dispatch_once方法來執行類似的初始化,
結論
當涉及到并發和并行代碼時,需要對代碼進行更仔細的檢查,始終讓你的團隊成員執行代碼審查,因為這樣的事情很容易就會被發現,
所有剛轉到Go語言的新開發人員都必須真正了解并發安全性如何作業以更好地改進其代碼,即使Go語言本身通過允許你在對并發性知識知之甚少的情況下設計并發代碼,也完成了許多繁重的作業,在某些情況下,單純的依靠語言特性也無能為力,你仍然需要在開發代碼時應用最佳實踐,
翻譯自http://marcio.io/2015/07/singleton-pattern-in-go/,考慮到可讀性部分內容有修改,
轉載請註明出處,本文鏈接:https://www.uj5u.com/houduan/47199.html
標籤:Go
上一篇:GORM入門指南
下一篇:選項模式
