記一次golang中sync.Map並發建立、讀取的問題

來源:互聯網
上載者:User

背景:

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

問題

大概是這樣

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

然後調用的時候的代碼大概就是這樣的:

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被並發訪問的時候(事實上push會經常被並發訪問),由於這把大鎖的存在,同時只能有一個協程在臨界區工作,效率是會被大大降低的。

解決方案:會帶來crash的最佳化

所以我們決定使用sync.Map來替代這個設計,然後出了第一版代碼,寫的非常簡單,只做了簡單的替換:

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)}

但是這樣還是有問題,相信經驗比較豐富的老哥一眼就能看出來,問題處在socketInter, ok = pushList.sockets.Load(ip)這行代碼上,如果map中沒有這個值,且有多個協程同事訪問到這行代碼,顯然這幾個協程的ok都會置位false,然後都進入第一個if代碼塊,建立多個socket執行個體,並且爭相覆蓋原有值。
單純解決這個問題也很簡單,就是使用sync.Map.LoadOrStore(key interface{}, value interface{}) (v interface{}, loaded bool)這個api,來原子地去做讀寫。
然而這還沒完,我們的寫入新值的操作不光是調用一個api建立socket就完了,還要有一系列的初始化操作,我們必須保證在初始化完成之前,其他通過Load拿到這個執行個體的協程無法真正訪問socket執行個體。
這時候顯然sync.Map內建的機制已經無法解決這個問題了,那麼我們必須尋求其他的手段,要麼鎖,要麼就sync.WaitGroup或者whatever的其他什麼東西。

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

後來經大佬指點,我在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中存放一個閉包函數,然後在閉包函數中等待本地的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)}

總結:

並發代碼中的競爭問題,每一行代碼的重入性都要深思熟慮啊。
時間略晚,懶得總結了(逃

相關文章

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.