go并发编程系列七:手写一个go线程池
背景:提到线程池,我们会有大概的印象,通常我们对线程池的理解是:一组活跃的线程,但是这种理解是片面的,不完整的,为此,在手写线程池之前,我们一定要明确线程池到底是什么?不要以我以为的方式去写代码。
一、线程池有以下要素组成
线程队列(Task Queue):用于存储待执行的任务,通常是一个先进先出(FIFO)队列。
工作线程池(Worker Threads):由一组已创建的线程组成,它们等待从任务队列中获取任务并执行它们。
管理器(Manager):负责将任务分配给空闲的工作线程,并监控线程池的状态。
我们说线程池是一组活跃的线程组,只答对了一部分,线程池还包括:线程队列。
先不考虑管理器,我们定义线程池模型:Pool,代码如下:
// Pool 表示线程池。 type Pool struct { Workers []Worker TaskQueue chan func() wg sync.WaitGroup }
定义工作线程池模型:Worker,代码如下:
// Worker 表示工作池中的工作人员。 type Worker struct { ID int }
二、为线程池模型Pool定义方法
根据线程池的组成要素,我们知道线程池Pool有一个队列TaskQueue,相应的Pool需要一个入队的方法,我们定义为Submit,代码如下:
// Submit 提交一个任务给线程池。 func (p *Pool) Submit(task func()) { p.wg.Add(1) p.TaskQueue <- task }
同时,还应该考虑关闭线程池Pool的情况,关闭线程池实际上就是关闭线程池的任务队列,而我们的任务队列是使用chan信道实现的,因此,我们可以简单的理解为:关闭线程池就是关闭线程池的任务队列,本质上是关闭线程池任务队列对应的信道。
我们定义一个名为Shutdown的方法,代码如下:
// Shutdown 关闭线程池,等待所有任务完成。 func (p *Pool) Shutdown() { close(p.TaskQueue) p.wg.Wait() }
要想使用线程池,还需要一个创建线程池的函数,我们定义为NewPool,代码如下:
// NewPool 创建一个新的线程池。 func NewPool(numWorkers int, taskQueueSize int) *Pool { pool := &Pool{ TaskQueue: make(chan func(), taskQueueSize), } pool.Workers = make([]Worker, numWorkers) for i := 0; i < numWorkers; i++ { pool.Workers[i] = Worker{ID: i} } return pool }
经过上述操作,线程池的雏形已经完成,为了让线程池能够处理队列里的任务,我们还需要为工作线程池Worker定义一个Start方法,代码如下:
// Start 启动工作人员,等待任务并执行。 func (w Worker) Start(p *Pool) { go func() { for task := range p.TaskQueue { task() // 执行任务函数 p.wg.Done() } }() }
至此,一个能处理队列任务的线程池模型Pool就完成了,但是,该怎么用呢?请看下面的讲解。
三、使用线程池Pool处理队列任务
使用线程池的步骤分为:
1:通过NewPool创建线程池
2:启动线程池的工作线程池
3:将待处理的任务交给线程池队列,由线程池里的工作线程池进行处理
func main() { pool := NewPool(3, 10) // 启动工作人员 for _, worker := range pool.Workers { worker.Start(pool) } // 提交一些任务到线程池 for i := 0; i < 5; i++ { id := i pool.Submit(func() { fmt.Printf("Task %d executed by Worker %d\n", id, id%3) }) } // 关闭线程池并等待所有任务完成 pool.Shutdown() }