|
|
51CTO旗下网站
|
|
移动端
创建专栏

Go Commons Pool发布以及Golang多线程编程问题总结

Apache Commons Pool的核心是基于LinkedBlockingDeque,idle对象都放在deque中。

作者:王渊命|2017-02-27 16:43

趁着元旦放假,整理了一下最近学习Golang时,『翻译』的一个Golang的通用对象池,放到 github (https://github.com/jolestar/go-commons-pool) 开源出来。之所以叫做『翻译』,是因为这个库的核心算法以及逻辑都是基于 Apache Commons Pool 的,只是把原来的Java『翻译』成了Golang。

前一段时间阅读kubernetes源码的时候,整体上学习了下Golang,但语言这种东西,学了不用,几个星期就忘差不多了。一次Golang实践群里聊天,有人问到Golang是否有通用的对象池,搜索了下,貌似没有比较完备的。当前Golang的pool有以下解决方案:

1.sync.Pool

sync.Pool 使用很简单,只需要传递一个创建对象的func即可。

  1. var objPool = sync.Pool{ 
  2. New: func() interface{} { 
  3. return NewObject()}} 
  4. 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是不可重入的。表现出来就是:

  1. ReentrantLock lock; 
  2.  
  3. public void a(){ 
  4.     lock.lock(); 
  5.     //do some thing 
  6.     lock.unlock(); 
  7.  
  8. public void b(){ 
  9.     lock.lock(); 
  10.     //do some thing 
  11.     lock.unlock(); 
  12.  
  13. public void all(){ 
  14.     lock.lock(); 
  15.     //do some thing 
  16.     a(); 
  17.     //do some thing 
  18.     b(); 
  19.     //do some thing 
  20.     lock.unlock(); 

上例all方法中嵌套调用a方法,虽然调用a方法的时候也需要锁,但因为all已经申请锁,并且该锁可重入,所以不会导致死锁。而同样的代码在Golang中是会导致死锁的:

  1. var lock sync.Mutex 
  2.  
  3. func a() { 
  4.     lock.Lock() 
  5.     //do some thing 
  6.     lock.Unlock() 
  7.  
  8. func b() { 
  9.     lock.Lock() 
  10.     //do some thing 
  11.     lock.Unlock() 
  12.  
  13. func all() { 
  14.     lock.Lock() 
  15.     //do some thing 
  16.     a() 
  17.     //do some thing 
  18.     b() 
  19.     //do some thing 
  20.     lock.Unlock() 

只能重构为下面这样的(命名不规范请忽略,只是demo)

  1. var lock sync.Mutex 
  2.  
  3. func a() { 
  4.     lock.Lock() 
  5.     a1() 
  6.     lock.Unlock() 
  7.  
  8. func a1() { 
  9.     //do some thing 
  10.  
  11. func b() { 
  12.     lock.Lock() 
  13.     b1() 
  14.     lock.Unlock() 
  15.  
  16. func b1() { 
  17.     //do some thing 
  18.  
  19. func all() { 
  20.     lock.Lock() 
  21.     //do some thing 
  22.     a1() 
  23.     //do some thing 
  24.     b1() 
  25.     //do some thing 
  26.     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。

  1. type TimeoutCond struct { 
  2.     L      sync.Locker 
  3.     signal chan int 
  4.  
  5. func NewTimeoutCond(l sync.Locker) *TimeoutCond { 
  6.     cond := TimeoutCond{L: l, signal: make(chan int, 0)} 
  7.     return &cond 
  8.  
  9. /** 
  10. return remain wait timeand is interrupt 
  11. */ 
  12. func (this *TimeoutCond) WaitWithTimeout(timeout time.Duration) (time.Duration, bool) { 
  13.     //wait should unlock mutex,  if not will cause deadlock 
  14.     this.L.Unlock() 
  15.     defer this.L.Lock() 
  16.     begin := time.Now().Nanosecond() 
  17.     select { 
  18.     case _, ok := <-this.signal: 
  19.         end := time.Now().Nanosecond() 
  20.         return time.Duration(end - begin), !ok 
  21.     case <-time.After(timeout): 
  22.         return 0, false 
  23.     } 

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对象的指针地址。

  1. type SyncIdentityMap struct { 
  2.     sync.RWMutex 
  3.     m map[uintptr]interface{} 
  4.  
  5. func (this *SyncIdentityMap) Get(key interface{}) interface{} { 
  6.     this.RLock() 
  7.     keyPtr := genKey(key
  8.     value := this.m[keyPtr] 
  9.     this.RUnlock() 
  10.     return value 
  11.  
  12. func genKey(key interface{}) uintptr { 
  13.     keyValue := reflect.ValueOf(key
  14.     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。

【本文为51CTO专栏作者“王渊命”的原创稿件,转载请通过作者微信公众号jolestar-blog获取授权】

戳这里,看该作者更多好文

【编辑推荐】

  1. 日本IT简报:2017年收入***的15个编程语言排行榜
  2. 2017年1月编程语言排行榜:Google Go荣获TIOBE年度编程语言
  3. 外媒速递:2017年关于编程技术发展趋势的11个大胆预测
  4. 老曹眼中的网络编程基础
  5. 未来最重要的三个能力:编程、写作、英语
【责任编辑:武晓燕 TEL:(010)68476606】

点赞 0
分享:
大家都在看
猜你喜欢