Golang標(biāo)準(zhǔn)庫探秘(一):sync 標(biāo)準(zhǔn)庫
2016-02-29 18:29:54 來源:郭軍 評(píng)論:0 點(diǎn)擊:
編者按:號(hào)稱”21世紀(jì)的c語言“的Golang逐漸被越來越多的公司關(guān)注和使用,而Golang標(biāo)準(zhǔn)庫則是編寫Golang語言程序代碼的基礎(chǔ),本文就將通過案例來講解sync這個(gè)標(biāo)準(zhǔn)庫。
在高并發(fā)或者海量數(shù)據(jù)的生產(chǎn)環(huán)境中,我們會(huì)遇到很多問題,GC(garbage collection,中文譯成垃圾回收)就是其中之一。說起優(yōu)化GC我們首先想到的肯定是讓對(duì)象可重用,這就需要一個(gè)對(duì)象池來存儲(chǔ)待回收對(duì)象,等待下次重用,從而減少對(duì)象產(chǎn)生數(shù)量。
標(biāo)準(zhǔn)庫原生的對(duì)象池
在Golang1.3版本便已新增了sync.Pool功能,它就是用來保存和復(fù)用臨時(shí)對(duì)象,以減少內(nèi)存分配,降低CG壓力,下面就來講講sync.Pool的基本用法。
type Pool struct { local unsafe.Pointer localSize uintptr New func() interface{}}
很簡潔,最常用的兩個(gè)函數(shù)Get/Put
var pool = &sync.Pool{New:func()interface{}{return NewObject()}} pool.Put() Pool.Get()
對(duì)象池在Get的時(shí)候沒有里面沒有對(duì)象會(huì)返回nil,所以我們需要New function來確保當(dāng)獲取對(duì)象對(duì)象池為空時(shí),重新生成一個(gè)對(duì)象返回
if p.New != nil { return p.New() }
在實(shí)現(xiàn)過程中還要特別注意的是Pool本身也是一個(gè)對(duì)象,要把Pool對(duì)象在程序開始的時(shí)候初始化為全局唯一。
對(duì)象池使用是較簡單的,但原生的sync.Pool有個(gè)較大的問題:我們不能自由控制Pool中元素的數(shù)量,放進(jìn)Pool中的對(duì)象每次GC發(fā)生時(shí)都會(huì)被清理掉。這使得sync.Pool做簡單的對(duì)象池還可以,但做連接池就有點(diǎn)心有余而力不足了,比如:在高并發(fā)的情景下一旦Pool中的連接被GC清理掉,那每次連接DB都需要重新三次握手建立連接,這個(gè)代價(jià)就較大了。
既然存在問題,那我們就自行構(gòu)建一個(gè)對(duì)象池吧。
對(duì)象池底層數(shù)據(jù)結(jié)構(gòu)
我們選擇用Golang的container標(biāo)準(zhǔn)包中的鏈表來做對(duì)象池的底層數(shù)據(jù)結(jié)構(gòu),它被封裝在container/list標(biāo)準(zhǔn)包里:
type Element struct { next, prev *Element list *List Value interface{} }
這里是定義了鏈表中的元素,這個(gè)標(biāo)準(zhǔn)庫實(shí)現(xiàn)的是一個(gè)雙向鏈表,并且已經(jīng)為我們封裝好了各種Front/Back方法。不過Front方法的實(shí)現(xiàn)和我們需要的還是有點(diǎn)差異,它只是返回鏈表中的第一個(gè)元素,但這個(gè)元素依然會(huì)鏈接在鏈表里,所以我們需要自行將它從鏈表中刪除,remove方法如下:
func (l *List) remove(e *Element) *Element { e.prev.next = e.next e.next.prev = e.prev e.next = nil e.prev = nil e.list = nil l.len-- return e }
這樣對(duì)象池的核心部分就完成了,但注意一下,從remove函數(shù)可以看出,container/list并不是線程安全的,所以在對(duì)象池的對(duì)象個(gè)數(shù)統(tǒng)計(jì)等一些功能會(huì)有問題。
原子操作并發(fā)安全
下面我們來自行解決并發(fā)安全的問題。Golang的sync標(biāo)準(zhǔn)包封裝了常用的原子操作和鎖操作。
sync/atomic封裝了常用的原子操作。所謂原子操作就是在針對(duì)某個(gè)值進(jìn)行操作的整個(gè)過程中,為了實(shí)現(xiàn)嚴(yán)謹(jǐn)性必須由一個(gè)獨(dú)立的CPU指令完成,該過程不能被其他操作中斷,以保證該操作的并發(fā)安全性。
`type ConnPool struct { conns []*conn mu sync.Mutex // lock protected len int32}`
在Golang中,我們常用的數(shù)據(jù)類型除了channel之外都不是線程安全的,所以在這里我們需要對(duì)數(shù)量(len)和切片(conns []*conn)做并發(fā)保護(hù)。至于需要幾把鎖做保護(hù),取決于實(shí)際場景,合理控制鎖的粒度。
接著介紹一下鎖操作,我們?cè)贕olang中常用的鎖——互斥鎖(Lock)和讀寫鎖(RWLock),互斥鎖和讀寫鎖的區(qū)別是:互斥鎖無論是讀操作還是寫操作都會(huì)對(duì)目標(biāo)加鎖也就是說所有的操作都需要排隊(duì)進(jìn)行,讀寫鎖是加鎖后寫操作只能排隊(duì)進(jìn)行但是可以并發(fā)進(jìn)行讀操作,要注意一點(diǎn)就是讀的時(shí)候?qū)懖僮魇亲枞模瑢懖僮鬟M(jìn)行的時(shí)候讀操作是阻塞的。類型sync.Mutex/sync.RWMutex的零值表示了未被鎖定的互斥量。也就是說,它是一個(gè)開箱即用的工具。只需對(duì)它進(jìn)行簡單聲明就可以正常使用了,例如(在這里以Mutex為例,相對(duì)于RWMutex也是同理):
var mu sync.Mutex mu.Lock() mu.Unlock()
鎖操作一定要成對(duì)出現(xiàn),也就是說在加鎖之后操作的某一個(gè)地方一定要記得釋放鎖,否則再次加鎖會(huì)造成死鎖問題
fatal error: all goroutines are asleep - deadlock
不過在Golang里這種錯(cuò)誤發(fā)生的幾率會(huì)很少,因?yàn)橛衐efer延時(shí)函數(shù)的存在
上面的代碼可以改寫為
var mu sync.Mutexmu.Lock()defer mu.Unlock()
在加鎖之后馬上用defer函數(shù)進(jìn)行解鎖操作,這樣即使下面我們只關(guān)心函數(shù)邏輯而在函數(shù)退出的時(shí)候忘記Unlock操作也不會(huì)造成死鎖,因?yàn)樵诤瘮?shù)退出的時(shí)候會(huì)自動(dòng)執(zhí)行defer延時(shí)函數(shù)釋放鎖。
標(biāo)準(zhǔn)庫中的并發(fā)控制-WaitGroup
sync標(biāo)準(zhǔn)包還封裝了其他很有用的功能,比如WaitGroup,它能夠一直等到所有的goroutine執(zhí)行完成,并且阻塞主線程的執(zhí)行,直到所有的goroutine(Golang中并發(fā)執(zhí)行的協(xié)程)執(zhí)行完成。文章開始我們說過,Golang是支持并發(fā)的語言,在其他goroutine異步運(yùn)行的時(shí)候主協(xié)程并不知道其他協(xié)程是否運(yùn)行結(jié)束,一旦主協(xié)程退出那所有的協(xié)程就會(huì)退出,這時(shí)我們需要控制主協(xié)程退出的時(shí)間,常用的方法:
1、time.Sleep()
讓主協(xié)程睡一會(huì),好方法,但是睡多久呢?不確定(最簡單暴力)
2、channel
在主協(xié)程一直阻塞等待一個(gè)退出信號(hào),在其他協(xié)程完成任務(wù)后給主協(xié)程發(fā)送一個(gè)信號(hào),主協(xié)程收到這個(gè)信號(hào)后退出
e := make(chan bool) go func() { fmt.Println("hello") e <- true }() <-e
3、waitgroup
給一個(gè)類似隊(duì)列似得東西初始化一個(gè)任務(wù)數(shù)量,完成一個(gè)減少一個(gè)
var wg sync.WaitGroup func main() { wg.Add(1) go func() { fmt.Println("hello") wg.Done() //完成 }() wg.Wait() }
這里要特別主要一下,如果waitGroup的add數(shù)量最終無法變成0,會(huì)造成死鎖,比如上面例子我add(2)但是我自始至終只有一個(gè)Done,那剩下的任務(wù)一直存在于wg隊(duì)列中,主協(xié)程會(huì)認(rèn)為還有任務(wù)沒有完成便會(huì)一直處于阻塞Wait狀態(tài),造成死鎖。
wg.Done方法其實(shí)在底層調(diào)用的也是wg.Add方法,只是Add的是-1
func (wg *WaitGroup) Done() { wg.Add(-1)}
我們看sync.WaitGroup的Add方法源碼可以發(fā)現(xiàn),底層的加減操作用的是我們上面提到的sync.atomic標(biāo)準(zhǔn)包來確保原子操作,所以sync.WaitGroup是并發(fā)安全的。
作者簡介
郭軍,奇虎360安全衛(wèi)士服務(wù)端技術(shù)團(tuán)隊(duì)成員,關(guān)注架構(gòu)設(shè)計(jì),GO語言等互聯(lián)網(wǎng)技術(shù)。
感謝姚夢(mèng)龍對(duì)本文的策劃和審校。
給InfoQ中文站投稿或者參與內(nèi)容翻譯工作,請(qǐng)郵件至editors@cn.infoq.com。也歡迎大家通過新浪微博(@InfoQ,@丁曉昀),微信(微信號(hào):InfoQChina)關(guān)注我們。
相關(guān)熱詞搜索:golang standard library part01 架構(gòu) & 設(shè)計(jì) 語言 & 開發(fā) 代碼庫 Golang 語言設(shè)計(jì) 語言
上一篇:為什么我要選擇使用Yarn來做Docker的調(diào)度引擎
下一篇:專訪Elabor8首席顧問Erwin:七大習(xí)慣培養(yǎng)高效成功組織

頻道總排行
- Cisco NetFlow v9為何無人問津?
- 技術(shù)專題:智能化運(yùn)維
- 開源代碼管理:如何安全地使用開源庫?
- Facebook架構(gòu)解讀
- IT運(yùn)維分析與海量日志搜索需要注意什么(1)
- 金山運(yùn)維肖力:如何將業(yè)務(wù)遷移到虛擬化環(huán)境并穩(wěn)定運(yùn)行(1)
- Apache Ignite(四):基于Ignite的分布式ID生成器
- CrazyEye,一款國人開源的堡壘機(jī)軟件(1)
- SDN時(shí)代的網(wǎng)絡(luò)管理系統(tǒng)會(huì)走向何方
- WOT2016吳兆松:Zabbix監(jiān)控自動(dòng)化的未來如何發(fā)展
頻道本月排行
- 8你消費(fèi)我買單——"漏洞"天使OneRASP...
- 7有了Jenkins,為什么還需要一個(gè)獨(dú)立...
- 6IT運(yùn)維分析與海量日志搜索需要注意什么(1)
- 5新浪微博王傳鵬:微博推薦架構(gòu)的演進(jìn)(1)
- 4史上最大機(jī)器學(xué)習(xí)數(shù)據(jù)集,雅虎對(duì)外開...
- 4雅虎開源可以提升流操作速度的DataSketches
- 4大眾點(diǎn)評(píng)高可用性系統(tǒng)運(yùn)維經(jīng)驗(yàn)分享
- 4云運(yùn)維如何選擇部署適合自身的IDC和...
- 4開源還是商用?十大云運(yùn)維監(jiān)控工具測(cè)...
- 4論開發(fā)與運(yùn)維沖突的根源、表現(xiàn)形式及...