前言
当初很多公司都在陆续的搭建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我的名字是 123123 执行结束我的名字是 456我的名字是 456我的名字是 456我的名字是 456我的名字是 456456 执行结束我是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来创立一个Taskfunc 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是相当相熟了,如果须要获取残缺代码,欢送关注公众号“程序员小饭”,回复"协程池"即可获取。