記一次golang中sync.Map并發(fā)創(chuàng)建、讀取的問題

背景:

我們有一個用go做的項目乒验,其中用到了zmq4進行通信愚隧,一個簡單的rpc過程,早期遠端是使用一個map去做ip和具體socket的映射锻全。

問題

大概是這樣

struct SocketMap {
    sync.Mutex
    sockets map[string]*zmq4.Socket
}

然后調(diào)用的時候的代碼大概就是這樣的:

func (pushList *SocketMap) push(ip string, data []byte) {
    pushList.Lock()
    defer pushList.UnLock()
    socket := pushList.sockets[string]
    if socket == nil {
      socket := zmq4.NewSocket()
      //do some initial operation like connect
      pushList.sockets[ip] = socket
    }
    socket.Send(data)
}

相信大家都能看出問題:當push被并發(fā)訪問的時候(事實上push會經(jīng)常被并發(fā)訪問)奸攻,由于這把大鎖的存在蒜危,同時只能有一個協(xié)程在臨界區(qū)工作,效率是會被大大降低的睹耐。

解決方案:會帶來crash的優(yōu)化

所以我們決定使用sync.Map來替代這個設(shè)計辐赞,然后出了第一版代碼,寫的非常簡單硝训,只做了簡單的替換:

struct SocketMap {
    sockets sync.Map
}

func (pushList *SocketMap) push(ip string, data []byte) {
    var socket *zmq4.Socket    
    socketInter, ok = pushList.sockets.Load(ip)
    if !ok {
      socket = zmq4.NewSocket()
      //do some initial operation like connect
      pushList.sockets.Store(ip, socket)
    } else {
      socket = socketInter.(*zmq4.Socket)
    }
    socket.Send(data)
}

乍一看似乎沒什么問題响委?但是跑起來總是爆炸,然后一看log窖梁,提示有個非法地址赘风。后來在github上才看到,zmq4.Socket不是線程安全的纵刘。上面的代碼恰恰會造成多個線程同時拿到socket實例邀窃,然后就crash了。

解決方案2: 加一把鎖也擋不住的沖突

然后怎么辦呢假哎?看來也只能加鎖了瞬捕,不過這次加鎖不能加到整個map上,否則還會有性能問題舵抹,那就考慮減小鎖的粒度吧肪虎,使用鎖包裝socket。這個時候我們的代碼也就呼之欲出了:

struct SocketMutex{
    sync.Mutex
    socket *zmq4.Socket
}
struct SocketMap {
    sockets sync.Map
}

func (pushList *SocketMap) push(ip string, data []byte) {
    var socket *SocketMutex    
    socketInter, ok = pushList.sockets.Load(ip)
    if !ok {
        socket = &{
          socket: zmq4.NewSocket()
        }
        //do some initial operation like connect
       pushList.sockets.Store(ip, newSocket)
    } else {
      socket = socketInter.(*SocketMutex)
    }
    socket.Lock()
    defer socket.Unlock()
    socket.socket.Send(data)
}

但是這樣還是有問題惧蛹,相信經(jīng)驗比較豐富的老哥一眼就能看出來扇救,問題處在socketInter, ok = pushList.sockets.Load(ip)這行代碼上,如果map中沒有這個值香嗓,且有多個協(xié)程同時訪問到這行代碼迅腔,顯然這幾個協(xié)程的ok都會置為false,然后都進入第一個if代碼塊靠娱,創(chuàng)建多個socket實例钾挟,并且爭相覆蓋原有值。
單純解決這個問題也很簡單饱岸,就是使用sync.Map.LoadOrStore(key interface{}, value interface{}) (v interface{}, loaded bool)這個api,來原子地去做讀寫徽千。
然而這還沒完苫费,我們的寫入新值的操作不光是調(diào)用一個api創(chuàng)建socket就完了,還要有一系列的初始化操作双抽,我們必須保證在初始化完成之前百框,其他通過Load拿到這個實例的協(xié)程無法真正訪問socket實例。
這時候顯然sync.Map自帶的機制已經(jīng)無法解決這個問題了牍汹,那么我們必須尋求其他的手段铐维,要么鎖柬泽,要么就sync.WaitGroup或者whatever的其他什么東西。

解決方案3: 閉包帶來的神奇體驗

后來經(jīng)大佬指點嫁蛇,我在encoder.go中看到了這么一段代碼:

 346 func typeEncoder(t reflect.Type) encoderFunc {                                 
 347     if fi, ok := encoderCache.Load(t); ok {                                     
 348         return fi.(encoderFunc)                                                
 349     }                                                                          
 350                                                                                
 351     // To deal with recursive types, populate the map with an                  
 352     // indirect func before we build it. This type waits on the                
 353     // real func (f) to be ready and then calls it. This indirect              
 354     // func is only used for recursive types.                                  
 355     var (                                                                      
 356         wg sync.WaitGroup                                                      
 357         f  encoderFunc                                                         
 358     )                                                                          
 359     wg.Add(1)                                                                  
 360     fi, loaded := encoderCache.LoadOrStore(t, encoderFunc(func(e *encodeState, v reflect.Value, opts encOpts) {
 361         wg.Wait()                                                              
 362         f(e, v, opts)                                                          
 363     }))                                                                        
 364     if loaded {                                                                
 365         return fi.(encoderFunc)                                                
 366     }                                                                          
 367                                                                                
 368     // Compute the real encoder and replace the indirect func with it.         
 369     f = newTypeEncoder(t, true)                                                
 370     wg.Done()                                                                  
 371     encoderCache.Store(t, f)                                                   
 372     return f                                                                   
 373 }          

豁然開朗锨并,我們可以在sync.Map中存放一個閉包函數(shù),然后在閉包函數(shù)中等待本地的sync.WaitGroup完成再返回實例睬棚。于是最終的代碼也就成型了第煮。

struct SocketMutex{
    sync.Mutex
    socket *zmq4.Socket
}
struct SocketMap {
    sockets sync.Map
}

func (pushList *SocketMap) push(ip string, data []byte) {
    type SocketFunc func()*SocketMutex
    var (
        socket *SocketMutex
        w sync.WaitGroup
    )
    socket = &SocketMutex {
      socket : zmq4.NewSocket()
    }    
    w.Add(1)
    socketf, ok = pushList.sockets.LoadOrStore(ip, SocketFunc(func()*SocketMutex) {
        w.Wait()
        return socket
    })
    if !ok {
        socket = &{
          socket: zmq4.NewSocket()
        }
        //do some initial operation like connect
       w.Done()
    } else {
      socket = socketInter.(*SockeFunc)()
    }
    socket.Lock()
    defer socket.Unlock()
    socket.socket.Send(data)
}

總結(jié):

并發(fā)代碼中的競爭問題,每一行代碼的重入性都要深思熟慮啊抑党。
總的來說要保持以下幾個準則:

(1) 不可重入訪問的系統(tǒng)資源包警,如socketfd, filefd,signalfd(事實上大多數(shù)這種系統(tǒng)資源都是不可重入的)等底靠,在使用無鎖結(jié)構(gòu)的容器害晦、讀寫鎖封裝的容器時,需要給每個資源單獨加鎖或者使用其他手段保證系統(tǒng)資源在臨界區(qū)受到有效保護暑中。

(2)如果有讀取壹瘟,如果為空則寫入的邏輯,需要使用能提供原子性保證的LoadOrSave調(diào)用痒芝,或者沒有的話俐筋,自己實現(xiàn)也要保證讀取和寫入過程整體的原子性;防止并發(fā)訪問Load調(diào)用時严衬,多個線程都返回否而創(chuàng)建多個實例澄者,然后在Save的時候又互相覆蓋∏肓眨——這個原則不光對成員是系統(tǒng)資源的時候生效粱挡,如果存放的是其他東西也同樣適用。

(3)如果資源創(chuàng)建完畢俄精,還需要其他的初始化過程询筏,則可以考慮在容器內(nèi)放置閉包,初始化過程使用sync.WaitGroup保護竖慧,在閉包中調(diào)用Wait方法等待初始化完成再給其他線程返回初始化好的實例嫌套。而初始化過程完成后,可以置換閉包函數(shù)圾旨,不再調(diào)用Wait方法踱讨,來減少可能的開銷。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末砍的,一起剝皮案震驚了整個濱河市痹筛,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖帚稠,帶你破解...
    沈念sama閱讀 206,311評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件谣旁,死亡現(xiàn)場離奇詭異,居然都是意外死亡滋早,警方通過查閱死者的電腦和手機榄审,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,339評論 2 382
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來馆衔,“玉大人瘟判,你說我怎么就攤上這事〗抢#” “怎么了拷获?”我有些...
    開封第一講書人閱讀 152,671評論 0 342
  • 文/不壞的土叔 我叫張陵,是天一觀的道長减细。 經(jīng)常有香客問我匆瓜,道長,這世上最難降的妖魔是什么未蝌? 我笑而不...
    開封第一講書人閱讀 55,252評論 1 279
  • 正文 為了忘掉前任驮吱,我火速辦了婚禮,結(jié)果婚禮上萧吠,老公的妹妹穿的比我還像新娘左冬。我一直安慰自己,他們只是感情好纸型,可當我...
    茶點故事閱讀 64,253評論 5 371
  • 文/花漫 我一把揭開白布拇砰。 她就那樣靜靜地躺著,像睡著了一般狰腌。 火紅的嫁衣襯著肌膚如雪除破。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,031評論 1 285
  • 那天琼腔,我揣著相機與錄音瑰枫,去河邊找鬼。 笑死丹莲,一個胖子當著我的面吹牛光坝,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播甥材,決...
    沈念sama閱讀 38,340評論 3 399
  • 文/蒼蘭香墨 我猛地睜開眼盯另,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了擂达?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 36,973評論 0 259
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎板鬓,沒想到半個月后悲敷,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 43,466評論 1 300
  • 正文 獨居荒郊野嶺守林人離奇死亡俭令,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 35,937評論 2 323
  • 正文 我和宋清朗相戀三年后德,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片抄腔。...
    茶點故事閱讀 38,039評論 1 333
  • 序言:一個原本活蹦亂跳的男人離奇死亡瓢湃,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出赫蛇,到底是詐尸還是另有隱情绵患,我是刑警寧澤,帶...
    沈念sama閱讀 33,701評論 4 323
  • 正文 年R本政府宣布悟耘,位于F島的核電站落蝙,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏暂幼。R本人自食惡果不足惜筏勒,卻給世界環(huán)境...
    茶點故事閱讀 39,254評論 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望旺嬉。 院中可真熱鬧管行,春花似錦、人聲如沸邪媳。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,259評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽悲酷。三九已至套菜,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間设易,已是汗流浹背逗柴。 一陣腳步聲響...
    開封第一講書人閱讀 31,485評論 1 262
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留顿肺,地道東北人戏溺。 一個月前我還...
    沈念sama閱讀 45,497評論 2 354
  • 正文 我出身青樓,卻偏偏與公主長得像屠尊,于是被迫代替她去往敵國和親旷祸。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 42,786評論 2 345

推薦閱讀更多精彩內(nèi)容