slava项目(1):实现Pool

78 阅读3分钟

slava是作者参与的一个Github开源项目,该项目主要是用Go语言构建一个高性能K-V云数据库。本文将介绍如何为slava构建一个简单的Pool。

什么是Pool

对于一些需要重复分配回收的对象来说,反复的分配回收通常会导致相当大的系统开销。为了避免这一开销,我们可以将这些对象在暂时不用时缓存起来而不是直接回收,等到下次需要时直接拿出来使用。Pool的作用就是用来缓存这些对象来实现资源复用。

Pool的最频繁的使用方式之一就是数据库中的连接池,如 go-xorm 的 连接池、go-redis 的 连接池。当一个对数据库的请求结束时,不关闭连接而是将连接放入连接池中,当别的请求需要建立连接时再拿出来用。除此之外,Pool的应用还有线程池,内存池等。

具体实现

Pool结构体

```
// Pool stores object for reusing, such as slava connection
type Pool struct {
   Config
   factory     func() (interface{}, error) // The factory function to create a new item
   finalizer   func(x interface{})         // The function to destroy an item
   idles       chan interface{}            // The channel to store the idle items
   waitingReqs []request                   // Requests to waiting for allocating an item
   activeCount uint                        // increases during creating item, decrease during destroying item
   mu          sync.Mutex
   closed      bool // Flag for the closed pool
}
```

其中,Config,factory, finalizer为用户在初始化时需要配置的设置,建立新对象的工厂函数以及销毁对象的函数。Config的配置如下:

```
type Config struct {
   MaxIdle   uint // The maximum number of idle items in a pool
   MaxActive uint // The maximum number of items that a pool can store
}
```

Pool底层用一个channel来存储空闲的对象,例如前文中提到的连接。事实上,在slava中最常用Pool来存储的对象就是连接,在下文中作者也会用连接来阐述Pool的各项功能。

取出连接

从Pool中取出连接分为两种情况,若是在idle中存在空闲的连接,则可以将其直接取出。如果没有空闲连接,则需要看当前Pool里的连接数量是否达到了MaxActive即Pool的最大上限。若是没达到,则可以直接利用工厂函数新建一个连接给需要这个连接的请求,但如果已经达到了上限,则只能等待其余使用中的连接重新返回后再将其分配,此时只能先将需要该连接的请求放入等待队列waitingReqs中。

代码实现

// Get try to get an idle item from pool
func (pool *Pool) Get() (interface{}, error) {
   pool.mu.Lock()
   if pool.closed {
      pool.mu.Unlock()
      return nil, ErrClosed
   }

   select {
   case item := <-pool.idles:
      pool.mu.Unlock()
      return item, nil
   default:
      // no pooled item, create one
      return pool.getOnNoIdle()
   }
}

getOnNoIdle() 函数就是用来处理上文中的第二种情况:

// getOnNoIdle try to create a new item or waiting for items being returned
// invoker should have pool.mu
func (pool *Pool) getOnNoIdle() (interface{}, error) {
   // Items reach the capacity of the pool, cannot create a new item
   if pool.activeCount >= pool.MaxActive {
      // waiting for item being returned
      req := make(chan interface{}, 1)
      pool.waitingReqs = append(pool.waitingReqs, req)
      pool.mu.Unlock()
      x, ok := <-req
      // No item can be arranged for the request (reach the item limit)
      if !ok {
         return nil, ErrMax
      }
      return x, nil
   }

   // create a new item
   pool.activeCount++ // hold a place for new item
   pool.mu.Unlock()
   x, err := pool.factory()
   if err != nil {
      // create failed return token
      pool.mu.Lock()
      pool.activeCount-- // release the holding place
      pool.mu.Unlock()
      return nil, err
   }
   return x, nil
}

放入连接

当一个连接被使用完毕需要重新放入Pool中时,毫无疑问现在这个连接就是空闲的了。此时又分为三种情况来处理这个空闲连接:

  1. 若是等待队列里已经有在等待连接的请求,这说明在之前该请求在取出连接时因为当时没有空闲连接也无法新创建连接而被搁置(取出连接中第二种情况的第二个可能性)。此时应直接将该空闲连接给这个请求,同时将该请求从等待队列中移除。
  2. 如果没有等待的请求,但是存储空闲连接的channel满了,那么只能将该连接直接销毁。
  3. 如上述两种情况都没有发现,就可以直接将连接存入idles等待复用了。

代码实现

// Put try to put an idle item into the pool
func (pool *Pool) Put(x interface{}) {
   pool.mu.Lock()
   // if the pool is closed, directly destroy the item
   if pool.closed {
      pool.mu.Unlock()
      pool.finalizer(x)
      return
   }
   // If there is waiting requests directly allocate the item to the first request
   if len(pool.waitingReqs) > 0 {
      req := pool.waitingReqs[0]
      copy(pool.waitingReqs, pool.waitingReqs[1:])
      pool.waitingReqs = pool.waitingReqs[:len(pool.waitingReqs)-1]
      req <- x
      pool.mu.Unlock()
      return
   }
   // Store the item or destroy it
   select {
   case pool.idles <- x:
      pool.mu.Unlock()
      return
   default:
      // reach max idle, destroy redundant items
      pool.mu.Unlock()
      pool.activeCount--
      pool.finalizer(x)
   }
}

关闭Pool

关闭Pool并没有什么复杂的逻辑,只需记得在关闭idles channel后,需要将里面剩下的连接全部手动销毁。

代码实现

// Close try to close the pool
func (pool *Pool) Close() {
   pool.mu.Lock()
   if pool.closed {
      pool.mu.Unlock()
      return
   }
   // Change the flag and close the idle item channel
   pool.closed = true
   close(pool.idles)
   pool.mu.Unlock()
   // Destroy all the left idle items in the pool
   for x := range pool.idles {
      pool.finalizer(x)
   }
}