为什么需要协程池?
1、我在处理几十万数据时候,因为无数次开启协程,报错协程开太多,cpu利用率飙升,无法再继续跑任务了
2、规定好,这个程序,只能开启多少个协程,例如3个协程去跑,就可以避免1的情况
3、非常高效和快速,根据我自己的业务逻辑,请求别的系统,存在修改,不存在新增,然后我这边要同步,3个协程去处理,1秒钟能处理20多个任务(当然这个也是要考虑别人系统的,别人系统是php写的)
大致架构
EntryChannel:入口方法,所有task任务都要扔到EntryChannel方法中,方便解耦,不可能直接扔队列JobsChannel中的,这样会导致代码太过拥挤、难管理,如果以后有修改,岂不是整个都得改?
JobsChannel:队列,为什么用队列呢?因为这个队列是先进先出的,排序功能有了,然后这个队列会一直去读取EntryChannel入口的task任务,利用golang的 for range 的特性,如果存在就获取,不存在就等待的特性
worker:处理任务的协程(goroutines),这里是负责处理业务逻辑的。
上代码:
文件
pool.go
package main
import (
"fmt"
"time"
)
// ------------------------------------ 有关task任务角色的功能 ---------------------------------
// 定义一个任务类型 task
type Task struct {
f func() error // 一个Task里面应该有一个具体的业务,这个业务的逻辑就是f这个函数
// 后续优化,可以在这里加一个任务优先级,然后在取的时候判断优先级
}
// 创建一个task任务
// arg_f 任务逻辑
func NewTask(arg_f func() error) *Task { // 返回一个任务
t := Task{
f: arg_f,
}
return &t
}
// task也需要一个执行业务的方法 -> 任务逻辑
func (t *Task) Execute() {
t.f() // 调用任务中已经绑定好的业务方法
}
// ------------------------------------ 有关task任务角色的功能 ---------------------------------
// ------------------------------------ 有关协程池pool角色的功能 ---------------------------------
// 定义一个pool协程池的类型
type Pool struct {
// 对外的task入口 EntryChannel -> 把task仍进入口
EntryChannel chan *Task
// 内部的task队列 JobsChannel
JobsChannel chan *Task
// 协程池中最大的worker的数量
Worker_num int
}
// 创建 Pool 的函数
func NewPool(cap int) *Pool {
// 创建一个Pool
p := Pool{
EntryChannel: make(chan *Task),
JobsChannel: make(chan *Task),
Worker_num: cap,
}
// 返回这个Pool
return &p
}
// 协程池创建一个worker,并且让这个worker去工作
func (p *Pool) Worker(worker_id int) {
// 一个worker具体的工作
// 1 永久的从jobsChannel去取任务
// for range 的语法就是 如果p.JobsChannel没有任务会停住,等到有数据又会继续执行
for task := range p.JobsChannel {
// task就是当前worker从 jobsChaneel中拿到的任务
// 2 一旦取到任务就去执行这个任务 , 后续优化这里可以加上优先级排序再执行任务
task.Execute()
fmt.Println("worker Id", worker_id, "执行完了一个任务")
}
}
// 让协程池开始真正的工作,这是一个协程池启动的方法
func (p *Pool) run() {
// 1 根据worker_num来创建worker去工作
for i := 0; i < p.Worker_num; i++ {
// 每一个worker就是一个goroutine
go p.Worker(i)
}
// 2 不断地从EntryChannel中取任务,将取道的任务发送给JobsChannel
for task := range p.EntryChannel {
// 一旦有task读到就要交给JobsChannel,交给他之后就会被上面的 Worker方法中的for range 取到
p.JobsChannel <- task
}
}
// 主函数
func main() {
// 1 创建一些任务
t := NewTask(func() error {
// 当前任务的业务 -> 打印出当前的系统时间
fmt.Println(time.Now())
return nil
})
// 2 创建一个 Pool协程池, 这个协程池最大的worker数量是4
p := NewPool(4)
// 执行了的任务数量
task_num := 0
// 3 将这些任务交给协程池Pool
go func() {
for {
// 不断向p中去写入任务t,每个任务都是打印当前时间
p.EntryChannel <- t // 向入口一直写任务
task_num += 1
fmt.Println("当前一共执行了", task_num, "任务")
}
}()
// 4 启动pool, 让Pool开始工作,此时pool会创建worker,让worker工作
// 为了让 p.run 执行,上面就一定要开启协程,不然只会死循环,永远都不会执行到p.run
p.run()
}
执行
go run pool.go

在公司业务中,我做了以下测试
没有用协程时候的请求时间

是不是非常慢?
当我用了协程并且设计了协程池的请求速度
怎么样?感受到它的威力了吗?
局限
当然,我这个简单的协程池功能,也有局限的,就是当你去另一个系统中请求时候,如果另一个系统报错了,或者另一个系统无法支持我这么多个请求而导致崩了,或者数据丢失了,这时候就需要有一个重新请求的逻辑,大家有更好的意见吗?
本文介绍了使用协程池处理大量数据的优势,如提高效率和限制并发数,以避免CPU利用率过高。通过Go语言实现了一个简单的协程池示例,展示了如何通过队列和工作协程来调度任务。测试结果显示,使用协程池显著提升了请求处理速度。然而,该方案也存在局限性,如对外部系统依赖的风险,需要考虑错误重试和数据一致性策略。
1169

被折叠的 条评论
为什么被折叠?



