足球资料库数据/孙祥/nba五佳球/足球直播哪个平台好 - cctv5今日现场直播

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)庫。

編者按:號(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)高效成功組織

分享到: 收藏