前言
当初很多公司都在陆续的搭建 golang 的语言栈,大家有没有想过为什么会呈现这种状况?一是因为 go 比拟适宜做中间件,还有一个起因就是 go 的并发反对比拟好,也就是咱们平时所谓的高并发,并发反对离不开协程,当然协程也不是乱用的,须要治理起来,治理协程的形式就是协程池,所以协程池也并没有那么神秘,明天咱们就来一步一步的揭开协程池的面纱,如果你没有接触过 go 的协程这块的话也没有关系,我会尽量写的具体。
# goroutine(协程)
先来看一个简略的例子
func go_worker(name string) {
for i := 0; i < 5; i++ {fmt.Println("我的名字是", name)
time.Sleep(1 * time.Second)
}
fmt.Println(name, "执行结束")
}
func main() {go_worker("123")
go_worker("456")
for i := 0; i < 5; i++ {fmt.Println("我是 main")
time.Sleep(1 * time.Second)
}
}
咱们在执行这段代码的时候,当然是依照程序执行
go_worker(“123”)->go_worker(“456”)-> 我是 main 执行
输入后果如下
我的名字是 123
我的名字是 123
我的名字是 123
我的名字是 123
我的名字是 123
123 执行结束
我的名字是 456
我的名字是 456
我的名字是 456
我的名字是 456
我的名字是 456
456 执行结束
我是 main
我是 main
我是 main
我是 main
我是 main
这样的执行是并行的,也就是说必须得等一个工作执行完结,下一个工作才会开始,如果某个工作比较慢的话,整个程序的效率是可想而知的,然而在 go 语言中,反对协程,所以咱们能够把下面的代码革新一下
func go_worker(name string) {
for i := 0; i < 5; i++ {fmt.Println("我的名字是", name)
time.Sleep(1 * time.Second)
}
fmt.Println(name, "执行结束")
}
func main() {go go_worker("123") // 协程
go go_worker("456") // 协程
for i := 0; i < 5; i++ {fmt.Println("我是 main")
time.Sleep(1 * time.Second)
}
}
咱们在不同的 go_worker 后面加上了一个 go,这样所有工作就异步的串行了起来,输入后果如下
我是 main
我的名字是 456
我的名字是 123
我的名字是 123
我是 main
我的名字是 456
我是 main
我的名字是 456
我的名字是 123
我是 main
我的名字是 456
我的名字是 123
我的名字是 456
我的名字是 123
我是 main
大家能够看到这样的话就是各自工作执行各自的事件,相互不影响,效率也失去了很大的晋升,这就是 goroutine
channel(管道)
有了协程之后就会带来一个新的问题,协程之间是如何通信的?于是就引出了管道这个概念,管道其实很简略,无非就是往里放数据,往外取数据而已
func worker(c chan int) {
num := <-c // 读取管道中的数据,并输入
fmt.Println("接管到参数 c:", num)
}
func main() {
//channel 的创立, 须要执行管道数据的类型,咱们这里是 int
c := make(chan int)
// 开拓一个协程 去执行 worker 函数
go worker(c)
c <- 2 // 往管道中写入 2
fmt.Println("main")
}
咱们能够看到上述例子,在 main 函数中,咱们定义了一个管道,为 int 类型,而且往里面写入了一个 2,而后在 worker 中读取管道 c,就能获取到 2
# 协程会引发的问题
既然 golang 中开启协程这么不便,那么会不会存在什么坑呢?
咱们能够看上图,理论业务中,不同的业务都开启不同的 goroutine 来执行,然而在 cpu 宏观层面上来讲,是串行的一个指令一个指令去执行的,只是执行的十分快而已,如果指令来的太多,cpu 的切换也会变多,在切换的过程中就须要耗费性能,所以协程池的次要作用就是治理 goroutine,限定 goroutine 的个数
协程池的实现
-
首先不同的工作,申请过去,间接往 entryChannel 中写入,entryChannel 再和 jobsChannel 建设通信
- 而后咱们固定开启三个协程 (不肯定是三个,只是用三个举例子),固定的从 jobsChannel 中读取数据,来进行工作解决。
- 其实实质上,channel 就是一道桥梁,做一个直达的作用,之所以要设计一个 jobsChannel 和 entryChannel,是为理解耦,entryChannel 能够齐全用做入口,jobsChannel 能够做更深刻的比方工作优先级,或者加锁,解锁等解决
# 代码实现
原理分明了,接下来咱们来具体看代码实现首先咱们来解决工作 task,task 无非就是业务中的各种工作,须要能实力化,并且执行,代码如下
// 定义工作 Task 类型, 每一个工作 Task 都能够形象成一个函数 type Task struct{f func() error // 一个 task 中必须蕴含一个具体的业务 } // 通过 NewTask 来创立一个 Task func NewTask(arg_f func() error) *Task{ t := Task{f:arg_f,} return &t } //Task 也须要一个执行业务的办法 func (t *Task) Execute(){t.f()// 调用工作中曾经绑定好的业务办法 }
接下来咱们来定义协程池
// 定义池类型 type Pool struct{ EntryChannel chan *Task WorkerNum int JobsChanel chan *Task } // 创立一个协程池 func NewPool(cap int) *Pool{ p := Pool{EntryChannel: make(chan *Task), JobsChanel: make(chan *Task), WorkerNum: cap, } return &p }
协程池须要创立 worker,而后一直的从 JobsChannel 外部工作队列中拿工作开始工作
// 协程池创立 worker 并开始工作 func (p *Pool) worker(workerId int){ //worker 一直的从 JobsChannel 外部工作队列中拿工作 for task := range p.JobsChanel{task.Execute() fmt.Println("workerId",workerId,"执行工作胜利") } }
EntryChannel 获取 Task 工作 func (p *Pool) ReceiveTask(t *Task){p.EntryChannel <- t}
// 让协程池开始工作 func (p *Pool) Run(){ //1: 首先依据协程池的 worker 数量限定,开启固定数量的 worker for i:=0; i<p.WorkerNum; i++{go p.worker(i) } //2: 从 EntryChannel 协程出入口取内部传递过去的工作 // 并将工作送进 JobsChannel 中 for task := range p.EntryChannel{p.JobsChanel <- task} //3: 执行结束须要敞开 JobsChannel 和 EntryChannel close(p.JobsChanel) close(p.EntryChannel) }
而后咱们看在 main 函数中
// 创立一个 task t:= NewTask(func() error{fmt.Println(time.Now()) return nil }) // 创立一个协程池,最大开启 5 个协程 worker p:= NewPool(3) // 开启一个协程,一直的向 Pool 输送打印一条工夫的 task 工作 go func(){ for {p.ReceiveTask(t)// 把工作推向 EntryChannel } }() // 启动协程池 p p.Run()
基于上述办法,咱们一个简略的协程池设计就实现了,当然在理论生产环境中这样做还是不够的,不过这些办法能手写进去,那对 golang 是相当相熟了,如果须要获取残缺代码,欢送关注公众号“程序员小饭”,回复 ” 协程池 ” 即可获取。