Go实现设计模式系列(8)——Go实现对象池模式

Object Pool Pattern(对象池模式)


对象池模式,对象被预先创建并初始化后放入对象池中,对象提供者,对象提供者就能利用已有的对象来处理请求,减少对象频繁创建所浪费的资源。例如数据库的连接池等等,基本都是创建后就被放入连接池中,后续的查询请求使用的都是连接池中的对象,从而加快了查询速度(不然每次查询都需要重新创建数据库连接对象,比较浪费)。

一句话解释,对象池模式下,程序在一开始就创建好了一批可用对象供使用

这种模式下,一般有两种角色,即对象池管理者和对象池用户。

对象池管理者负责管理整个对象池,包括初始化对象池,扩充对象池的大小,重置归还对象的状态等等。

对象池用户负责从对象池中获取一个对象,用完之后一般需要归还整这个对象。

一般被放入对象池中的对象包括Socket对象,数据库连接对象(连接池),线程对象(线程池)等等。

五大要素


来看看对象池模式的五大要素:

  • 模式名称:对象池模式
  • 目的(What):在程序刚开始创建一批可用对象供使用。
  • 解决的问题(Why):当我们频繁需要获取一个对象,并且该对象的频繁创建和销毁可能会导致系统性能较高的开销时,使用这种模式,避免对象的重复创建和销毁带来的资源开销
  • 解决方案(How):一个对象池管理者,负责管理对象;有若干个对象池用户,从对象池中获取对象,用完之后归还。
  • 解决效果
    • 优点:
      1. 复用池中对象,消除创建对象、销毁对象所产生的内存开销、CPU开销以及(若跨网络)产生的网络开销
    • 缺点:
      1. 如果是一些轻中量级的对象,创建/销毁对象的开销几乎可以忽略不计,没必要使用这种模式,增加程序的复杂度
      2. 并发环境中,多个线程可能(同时)需要获取对象,这个时候必然要引起互斥锁,获取互斥锁(加锁/解锁)的操作产生的开销往往会比创建/销毁对象产生的开销还要大。
      3. 由于一般来说,池中的对象是有限的,这个势必会成为系统可伸缩性的一个瓶颈
      4. 很难正确的设定对象池的大小,如果太小则不起作用,如果太大,则内存占用资源过高。

Go实现对象池模式


Go语言内部有sync.pool包实现了对象池的概念,但是这个设计出来的目的是优化GC的,因为它存储的对象随时都有可能被GC回收掉,具体感兴趣地可以去了解一下该包的用法,这篇文章就不对该包作详细解答了,因为我们的主要目的还是了解对象池模式。因此下面我们会自己实现一个对象池。

场景

当前的场景是我们需要实现一个协程池,我们开始给这个协程池固定一个协程数量,每个协程被用于去执行一项任务(Task),假设当前有1万个任务,我们必不可能用只用一个协程去执行,我们开启若干个协程去不断地轮询执行这些任务

object_pool.go:

package objectPool

import (
	"fmt"
	"sync"
)

// 任务对象,任务具体做什么由f决定
type Task struct {
	f func() error
	id int
}

// 创建一个任务对象
func NewTask(f func() error,id int) *Task {
	return &Task{
		f:  f,
		id: id,
	}
}

// 执行该任务对象
func (t *Task) Execute() error {
	return t.f()
}

// 协程池对象
type Pool struct {
	EntryChannel chan *Task

	// 协程池中worker的数量
	workerNum int

	// 协程池内部的任务就绪队列
	JobsChannel chan *Task
	
  // 加waitgroup为了在测试的时候控制程序主动退出
	wg *sync.WaitGroup
}

// 创建一个协程池
func NewPool(cap int, wg *sync.WaitGroup) *Pool {
	return &Pool{
		EntryChannel: make(chan *Task),
		workerNum:    cap,
		JobsChannel:  make(chan *Task),
		wg:           wg,
	}
}

func (p *Pool) SetTask(task *Task) {
	p.EntryChannel <- task
}

func (p *Pool) worker(workID int) {
	for task := range p.JobsChannel {
		// worker从jobsChannel中获取task
		err := task.Execute()
		if err != nil {
			fmt.Printf("workerID: %d, failed to execute task id %d ,err: %v\n", workID, task.id,err)
		} else {
			fmt.Printf("workerID: %d, the task id %d was successfully executed\n", workID,task.id)
		}
		// 完成一个任务done
		p.wg.Done()
	}

}

func (p *Pool) Run() {
	for i := 0; i < p.workerNum; i++ {
		go p.worker(i)
	}

	for task := range p.EntryChannel {
		p.JobsChannel <- task
	}
}

func (p *Pool) Stop() {
	close(p.JobsChannel)
	close(p.EntryChannel)
}

object_pool_test.go:

package objectPool

import (
	"fmt"
	"sync"
	"testing"
	"time"
)

func TestObjectPool(t *testing.T) {
	// 创建一个拥有10个worker的协程池
	start := time.Now()
	defer func() {
		end := time.Now()
		fmt.Printf("it takes %f seconds\n",end.Sub(start).Seconds())
	}()

	wg := &sync.WaitGroup{}
	p := NewPool(10,wg)

	go p.Run()

	// 创建100个task
	for i := 0; i < 100; i++ {
		wg.Add(1)
		t := NewTask(func() error {
			// 为了显示出协程池的效果,这里每个任务sleep一秒
			time.Sleep(time.Second)
			return nil
		},i)
		p.SetTask(t)
	}

	wg.Wait()
	p.Stop()
}

执行测试结果:

=== RUN   TestObjectPool
workerID: 8, the task id 8 was successfully executed
workerID: 2, the task id 4 was successfully executed
...............
...............
workerID: 6, the task id 2 was successfully executed
workerID: 6, the task id 97 was successfully executed
workerID: 2, the task id 99 was successfully executed
it takes 10.030659 seconds
--- PASS: TestObjectPool (10.03s)
PASS

上述例子在做测试的时候,我特意在每个task任务中 sleep了1秒,保证它们至少执行1秒再推出。如果每个任务中只是打印一个日志,这样我们在测试的时候会发现,可能协程池开的worker数越少越好,因为这个时候在一开始创建多个worker的开销已经远远超过每个任务执行的开销了。

因此协程数不是开的越多越好,具体开多少个是需要在指定场景进行性能测试才能决定的

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值