背景:
我們有一個用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)}
總結:
並發代碼中的競爭問題,每一行代碼的重入性都要深思熟慮啊。
時間略晚,懶得總結了(逃