Go Commons Pool發布以及Golang多線程編程問題總結

jopen 8年前發布 | 46K 次閱讀 Google Go/Golang開發

趁著元旦放假,整理了一下最近學習Golang時,『翻譯』的一個Golang的通用對象池,放到 github Go Commons Pool開源出來。之所以叫做『翻譯』,是因為這個庫的核心算法以及邏輯都是基于 Apache Commons Pool 的,只是把原來的Java『翻譯』成了Golang。

前一段時間閱讀kubernetes源碼的時候,整體上學習了下Golang,但語言這種東西,學了不用,幾個星期就忘差不多了。一次Golang實踐群里聊天,有人問到Golang是否有通用的對象池,搜索了下,貌似沒有比較完備的。當前Golang的pool有以下解決方案:

  1. sync.Pool
    sync.Pool 使用很簡單,只需要傳遞一個創建對象的func即可。

     var objPool = sync.Pool{
     New: func() interface{} {
     return NewObject()}}
     p := objPool.Get().(*Object)

    但sync.Pool只解決對象復用的問題,pool中的對象生命周期是兩次gc之間,gc后pool中的對象會被回收,使用方不能控制對象的生命周期,所以不適合用在連接池等場景。

  2. 通過container/list來實現自定義的pool,比如redigo 就使用這種辦法。但這些自定義的pool大多都不是通用的,功能也不完備。比如redigo當前沒有獲取連接池的超時機制,參看這個issue Blocking with timeout when Get PooledConn

而Java中的commons pool,功能比較完備,算法和邏輯也經過驗證,使用也比較廣泛,所以就直接『翻譯』過來,順便練習Golang的語法。

作為一個通用的對象池,需要包含以下主要功能:

  1. 對象的生命周期可以精確控制 Pool提供機制允許使用方自定義對象的創建/銷毀/校驗邏輯
  2. 對象的存活數量可以精確控制 Pool提供設置存活數量以及時長的配置
  3. 獲取對象有超時機制避免死鎖,方便使用方實現failover 以前也遇到過許多線上故障,就是因為連接池的設置或者實現機制有缺陷導致的。

Apache Commons Pool的核心是基于LinkedBlockingDeque,idle對象都放在deque中。之所以是deque,而不是queue,是因為它支持LIFO(last in, first out) /FIFO(first in, first out) 兩種策略獲取對象。然后有個包含所有對象的Map,key是用戶自定義對象,value是PooledObject,用于校驗Return Object的合法性,后臺定時abandoned時遍歷,計算活躍對象數等。超時是通過Java鎖的wait timeout機制實現的。

下面總結下將Java翻譯成Golang的時候遇到的多線程問題

遞歸鎖或者叫可重入鎖(Recursive Lock)

Java中的synchronized關鍵詞以及LinkedBlockingDequeu中用到的ReentrantLock,都是可重入的。而Golang中的sync.Mutex是不可重入的。表現出來就是:

ReentrantLock lock;

public void a(){
    lock.lock();
    //do some thing
    lock.unlock();
}

public void b(){
    lock.lock();
    //do some thing
    lock.unlock();
}

public void all(){
    lock.lock();
    //do some thing
    a();
    //do some thing
    b();
    //do some thing
    lock.unlock();
}

上例all方法中嵌套調用a方法,雖然調用a方法的時候也需要鎖,但因為all已經申請鎖,并且該鎖可重入,所以不會導致死鎖。而同樣的代碼在Golang中是會導致死鎖的:

var lock sync.Mutex

func a() {
    lock.Lock()
    //do some thing
    lock.Unlock()
}

func b() {
    lock.Lock()
    //do some thing
    lock.Unlock()
}

func all() {
    lock.Lock()
    //do some thing
    a()
    //do some thing
    b()
    //do some thing
    lock.Unlock()
}

只能重構為下面這樣的(命名不規范請忽略,只是demo)

var lock sync.Mutex

func a() {
    lock.Lock()
    a1()
    lock.Unlock()
}

func a1() {
    //do some thing
}

func b() {
    lock.Lock()
    b1()
    lock.Unlock()
}

func b1() {
    //do some thing
}

func all() {
    lock.Lock()
    //do some thing
    a1()
    //do some thing
    b1()
    //do some thing
    lock.Unlock()
}

Golang的核心開發者認為可重入鎖是不好的設計,所以不提供,參看Recursive (aka reentrant) mutexes are a bad idea。于是我們使用鎖的時候就需要多注意嵌套以及遞歸調用。

鎖等待超時機制

Golang的 sync.Cond 只有Wait,沒有如Java中的Condition的超時等待方法await(long time, TimeUnit unit)。這樣就沒法實現LinkBlockingDeque的 pollFirst(long timeout, TimeUnit unit) 這樣的方法。有人提了issue,但被拒絕了 sync: add WaitTimeout method to Cond。 所以只能通過channel的機制模擬了一個超時等待的Cond。完整源碼參看 go-commons-pool/concurrent/cond.go

type TimeoutCond struct {
    L      sync.Locker
    signal chan int
}

func NewTimeoutCond(l sync.Locker) *TimeoutCond {
    cond := TimeoutCond{L: l, signal: make(chan int, 0)}
    return &cond
}

/**
return remain wait time, and is interrupt
*/
func (this *TimeoutCond) WaitWithTimeout(timeout time.Duration) (time.Duration, bool) {
    //wait should unlock mutex,  if not will cause deadlock
    this.L.Unlock()
    defer this.L.Lock()
    begin := time.Now().Nanosecond()
    select {
    case _, ok := <-this.signal:
        end := time.Now().Nanosecond()
        return time.Duration(end - begin), !ok
    case <-time.After(timeout):
        return 0, false
    }
}

Map機制的問題

這個問題嚴格的說不屬于多線程的問題。雖然Golang的map不是線程安全的,但通過mutex封裝一下也很容易實現。關鍵問題在于我們前面提到的,pool中用于維護全部對象的map,key是用戶自定義對象,value是PooledObject。而Golang對map的key的約束是:go-spec#Map_types

The comparison operators == and != must be fully defined for operands of the key type; thus the key type must not be a function, map, or slice. If the key type is an interface type, these comparison operators must be defined for the dynamic key values; failure will cause a run-time panic.

也就是說key中不能包含不可比較的值,比如 slice, map, and function。而我們的key是用戶自定義的對象,沒辦法進行約束。于是借鑒Java的IdentityHashMap的思路,將key轉換成對象的指針地址,實際上map中保存的是key對象的指針地址。

type SyncIdentityMap struct {
    sync.RWMutex
    m map[uintptr]interface{}
}

func (this *SyncIdentityMap) Get(key interface{}) interface{} {
    this.RLock()
    keyPtr := genKey(key)
    value := this.m[keyPtr]
    this.RUnlock()
    return value
}

func genKey(key interface{}) uintptr {
    keyValue := reflect.ValueOf(key)
    return keyValue.Pointer()
}

同時,這樣做的缺點是Pool中存的對象必須是指針,不能是值對象。比如string,int等對象是不能保存到Pool中的。

其他的關于多線程的題外話

Golang的test -race 參數非常好用,通過這個參數,發現了幾個data race的bug,參看commit fix data race test error

Go Commons Pool后續工作

  1. 繼續完善測試用例,測試用例當前已經完成了大約一半多,覆蓋率88%。『翻譯』的時候,主體代碼相對來說寫起來很快,但測試用例就比較麻煩多了,多線程情況下調試也比較復雜。一般基礎庫的測試用例代碼是核心邏輯代碼的2-3倍。
  2. 做下benchmark。核心算法上應該沒啥問題,都是進過驗證的。但用channel模擬timeout的機制上可能有瓶頸。這塊要考慮timer的復用機制。參看 Terry-Mao/goim
  3. 上兩項完成了,就可以準備發布個正式版本,可以通過這個pool改進下redigo。

來自: http://jolestar.com/go-commons-pool-and-go-concurrent/

 本文由用戶 jopen 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
 轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
 本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!