都知道Golang本身是支持高并发的,其实很多语言都支持并发,像Java也可以创建多个线程(Thread),但是为什么要强调Golang支持高并发呢?那是因为Golang并发执行100w个协程(coroutine)也不会觉得特备吃力,但是Java并发执行1w个线程(Thread)其性能下降就显而易见了,因此支持高并发也是Golang的特性之一。
Golang中并发执行是依靠协程(coroutine)来实现的,main包中的main函数是程序执行的入口,它本身就是一个主协程。如果我们处理某些任务的时候没有创建子协程去执行而仅仅在主协程中执行,那么这个运行模式就是串行(即一件事做完才能做下一件事,例:一个用户请求过来之后会被处理,假如当前的请求还没处理完毕,但后面接连来了其他的请求,这时候必须等正在处理的请求处理完毕并作出响应,才能轮到下一个请求执行,未执行的请求将被阻塞在外部等待),可想而知这个效率是非常低下的,你敢想象多用户量的网站使用串行模式来处理用户请求吗?(这么做的后果是绝大多数用户的网络请求将超时而失效),因此串行来做网络请求不可行。并行(多个任务在同一时刻同时执行)的话也不可能实现,因为没有那么多核的CPU。所以只剩下并发(多个任务在同一个时间段内交替间隔执行)。Golang说到并发就会想到协程(与线程的作用相似,被称为“轻量级线程”),每个协程中执行的都是一个函数(因为Golang执行的最小单位是函数),该函数不需要返回值(即使有也没有意义,通常不设返回值)。
好了,都知道Golang使用并发来模式执行任务效率是比较高的,特别是对于并发量大的应用而言这个优点更为明显,但是我们真的会给每个任务都开一个子协程吗?当然如果并发量在可接受范围内(如:学生选课时的并发量等),那么对于每个请求都开一个子协程也没有什么太大的性能上的问题。但是像某宝等用户量大的网站这种做法显然不够理想,因为对于每个请求都开一个子协程,并发执行的子协程数量达到一定规模,程序的性能会明显下降,影响用户体验。另一个是并发执行的子协程越多,CPU做的无用功也会越多。因此最好的做法就是我们约定协程的数量,并且事先启动这些协程,这样一旦有任务过来就会被分配到空闲子协程执行,如果所有子协程都处理忙碌状态,那么就需要等待某个子协程空闲出来再进行任务的调用和执行。
package main
import (
"fmt"
"time"
)
var total int
//任务,do是任务的处理逻辑函数
type Task struct {
do func(i int) error
}
//任务调度执行的方法
func (task *Task)Execute(i int)error{
return task.do(i)
}
//协程池
type Pool struct {
outerTaskChannel chan *Task //外部任务通道
innerTaskChannel chan *Task //内部任务通道
count int //并发子协程数量
}
//子协程逻辑处理方法
func (pool *Pool)working(i int){
for task := range pool.innerTaskChannel{
task.Execute(i)
}
}
//协程池启用方法
func (pool *Pool)run(){
for i := 0;i < pool.count;i++ {
go pool.working(i)
}
for task := range pool.outerTaskChannel {
pool.innerTaskChannel <- task
}
}
func main(){
//任务构建
task := Task{
do:func(i int)error{
total++
fmt.Println("协程:",i,"---任务:",total,"---时间:",time.Now().Format("2006-01-02 15:04:05"))
return nil
},
}
//构建协程池
pool := Pool{
outerTaskChannel:make(chan *Task),
innerTaskChannel:make(chan *Task),
count:5,
}
go func(){
//模拟多个任务请求过来
for {
pool.outerTaskChannel <- &task
}
}()
pool.run()
}
协程池中构建内部任务通道和外部任务通道,按照逻辑来说基本上只需要一个任务通道就够用了,但是这里用两个是为了避免内部任务通道中的待执行任务被暴露到外部,因为我们的协程池是整个应用共用的,对于不相关的任务而言我们不希望暴露其他的任务信息出来,因此这里使用两个任务通道来隔离。