主頁 > 知識庫 > golang中sync.Map并發(fā)創(chuàng)建、讀取問題實戰(zhàn)記錄

golang中sync.Map并發(fā)創(chuàng)建、讀取問題實戰(zhàn)記錄

熱門標(biāo)簽:濱州自動電銷機(jī)器人排名 黃岡人工智能電銷機(jī)器人哪個好 建造者2地圖標(biāo)注 汕頭小型外呼系統(tǒng) 阿里云ai電話機(jī)器人 浙江高頻外呼系統(tǒng)多少錢一個月 惠州電銷防封電話卡 鄭州亮點科技用的什么外呼系統(tǒng) 釘釘有地圖標(biāo)注功能嗎

背景:

我們有一個用go做的項目,其中用到了zmq4進(jìn)行通信,一個簡單的rpc過程,早期遠(yuǎn)端是使用一個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)
}

相信大家都能看出問題:當(dāng)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,然后都進(jìn)入第一個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ī)制已經(jīng)無法解決這個問題了,那么我們必須尋求其他的手段,要么鎖,要么就sync.WaitGroup或者whatever的其他什么東西。

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

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

 func typeEncoder(t reflect.Type) encoderFunc {     
 if fi, ok := encoderCache.Load(t); ok {     
  return fi.(encoderFunc)      
 }          
          
 // To deal with recursive types, populate the map with an   
 // indirect func before we build it. This type waits on the  
 // real func (f) to be ready and then calls it. This indirect  
 // func is only used for recursive types.     
 var (         
  wg sync.WaitGroup       
  f encoderFunc        
 )          
 wg.Add(1)         
 fi, loaded := encoderCache.LoadOrStore(t, encoderFunc(func(e *encodeState, v reflect.Value, opts encOpts) {
  wg.Wait()        
  f(e, v, opts)        
 }))         
 if loaded {        
  return fi.(encoderFunc)      
 }          
           
 // Compute the real encoder and replace the indirect func with it.  
 f = newTypeEncoder(t, true)      
 wg.Done()         
 encoderCache.Store(t, f)       
 return f         
 }  

豁然開朗,我們可以在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ā)代碼中的競爭問題,每一行代碼的重入性都要深思熟慮啊。

總的來說要保持以下幾個準(zhǔn)則:

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

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

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

好了,以上就是這篇文章的全部內(nèi)容了,希望本文的內(nèi)容對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,如果有疑問大家可以留言交流,謝謝大家對腳本之家的支持。

您可能感興趣的文章:
  • golang中使用sync.Map的方法
  • 深度解密 Go 語言中的 sync.map
  • Go 并發(fā)讀寫 sync.map 詳細(xì)

標(biāo)簽:昭通 瀘州 阿壩 泰安 東營 滄州 晉中 駐馬店

巨人網(wǎng)絡(luò)通訊聲明:本文標(biāo)題《golang中sync.Map并發(fā)創(chuàng)建、讀取問題實戰(zhàn)記錄》,本文關(guān)鍵詞  golang,中,sync.Map,并發(fā),創(chuàng)建,;如發(fā)現(xiàn)本文內(nèi)容存在版權(quán)問題,煩請?zhí)峁┫嚓P(guān)信息告之我們,我們將及時溝通與處理。本站內(nèi)容系統(tǒng)采集于網(wǎng)絡(luò),涉及言論、版權(quán)與本站無關(guān)。
  • 相關(guān)文章
  • 下面列出與本文章《golang中sync.Map并發(fā)創(chuàng)建、讀取問題實戰(zhàn)記錄》相關(guān)的同類信息!
  • 本頁收集關(guān)于golang中sync.Map并發(fā)創(chuàng)建、讀取問題實戰(zhàn)記錄的相關(guān)信息資訊供網(wǎng)民參考!
  • 推薦文章