实例代码解析
并发
:严格意义上来说并发分为内核态,和用户态。
内核态
:单个CPU处理一个进程,多个CPU 同时处理多个进程
用户态
:单个的CPU的情况下,用户通过编程,在一个进程中通过多尔线程来实现并发
Go 的并发属于 CSP 并发模型的一种实现,
CSP 并发模型的核心概念是:“不要通过共享内存来通信,而应该通 过通信来共享内存”
。
这在 Go 语言中的实现就是 Goroutine 和 Channel。
实例解析一:
go实现一个线程池假定场景:
某平台的一个Restful 接口,该接口被调用后去处理一些事情,但是客户端需要快速得到响应,业务的处理过程将在后台继续进行。程序中我们可以开启一个协程去处理
func stratProcessor(){
go dosomething()
}
对于一定量的负载,这种处理没问题,但是当大量请求时,程序会不断创建新的协程,进而导致程序崩溃所以有必要去控制创建goroutine 的数量。
实例解析二:
package main
import (
"fmt"
"runtime"
"time"
)
func main(){
dataChan:=make(chan int,100)
go func(){
for{
select{
case data:=<-dataChan:
fmt.Println("data:",data)
//这里延迟是模拟处理数据的耗时
time.Sleep(1 * time.Second)
}
}
}()
//填充数据
for i:=0;i<100;i++{
dataChan<-i
}
//这里循环打印查看协程个数
for {
fmt.Println("runtime.NumGoroutine() :", runtime.NumGoroutine())
time.Sleep(2 * time.Second)
}
}
严格意义上来说上面处理并不是真正的并发处理。这种方法使用了缓冲队列一定程度上了提高了并发,但也是治标不治本,大规模并发只是推迟了问题的发生时间。当请求速度远大于队列的处理速度时,缓冲区很快被打满,后面的请求一样被堵塞了。
实例解析三:
真正意义上的并发处理请求
思想:领导 — > 工人 ---- > 任务
领导便是协程池,工人便是一个个协程,任务就是工人要做的事
通过一个二级通道实现 高并发控制
package main
import (
"fmt"
"runtime"
"time"
)
//任务
type Job interface {
Do()//do something...
}
//----------------------------------------------
//worker 工人
type Worker struct {
JobQueue chan Job //任务队列
Quit chan bool //停止当前任务
}
//新建一个 worker 通道实例 新建一个工人
func NewWorker() Worker {
return Worker{
JobQueue: make(chan Job), //初始化工作队列为null
Quit: make(chan bool),
}
}
/*
整个过程中 每个Worker(工人)都会被运行在一个协程中,
在整个WorkerPool(领导)中就会有num个可空闲的Worker(工人),
当来一条数据的时候,领导就会小组中取一个空闲的Worker(工人)去执行该Job,
当工作池中没有可用的worker(工人)时,就会阻塞等待一个空闲的worker(工人)。
每读到一个通道参数 运行一个 worker
*/
func (w Worker) Run(wq chan chan Job) {
//这是一个独立的协程 循环读取通道内的数据,
//保证 每读到一个通道参数就 去做这件事,没读到就阻塞
go func() {
for {
wq <- w.JobQueue //注册工作通道 到 线程池
select {
case job := <-w.JobQueue: //读到参数
job.Do()
case <-w.Quit: //终止当前任务
return
}
}
}()
}
//----------------------------------------------
//workerpool 领导
type WorkerPool struct {
workerlen int //线程池中 worker(工人) 的数量
JobQueue chan Job //线程池的 job 通道
WorkerQueue chan chan Job
}
func NewWorkerPool(workerlen int) *WorkerPool {
return &WorkerPool{
workerlen: workerlen,//开始建立 workerlen 个worker(工人)协程
JobQueue: make(chan Job), //工作队列 通道
WorkerQueue: make(chan chan Job, workerlen), //最大通道参数设为 最大协程数 workerlen 工人的数量最大值
}
}
//运行线程池
func (wp *WorkerPool) Run() {
//初始化时会按照传入的num,启动num个后台协程,然后循环读取Job通道里面的数据,
//读到一个数据时,再获取一个可用的Worker,并将Job对象传递到该Worker的chan通道
fmt.Println("初始化worker")
for i := 0; i < wp.workerlen; i++ {
//新建 workerlen 20万 个 worker(工人) 协程(并发执行),每个协程可处理一个请求
worker := NewWorker() //运行一个协程 将线程池 通道的参数 传递到 worker协程的通道中 进而处理这个请求
worker.Run(wp.WorkerQueue)
}
// 循环获取可用的worker,往worker中写job
go func() { //这是一个单独的协程 只负责保证 不断获取可用的worker
for {
select {
case job := <-wp.JobQueue: //读取任务
//尝试获取一个可用的worker作业通道。
//这将阻塞,直到一个worker空闲
worker := <-wp.WorkerQueue
worker <- job//将任务 分配给该工人
}
}
}()
}
//----------------------------------------------
type Dosomething struct {
Num int
}
func (d *Dosomething) Do() {
fmt.Println("开启线程数:", d.Num)
time.Sleep(1 * 1 * time.Second)
}
func main() {
//设置最大线程数
num := 100 * 100 * 20
// 注册工作池,传入任务
// 参数1 初始化worker(工人)并发个数 20万个
p := NewWorkerPool(num)
p.Run()//有任务就去做,没有就阻塞,任务做不过来也阻塞
//datanum := 100 * 100 * 100 * 100 //模拟百万请求
datanum := 100 * 100
go func() { //这是一个独立的协程 保证可以接受到每个用户的请求
for i := 1; i <= datanum; i++ {
sc := &Dosomething{Num: i}
p.JobQueue <- sc //往线程池 的通道中 写参数 每个参数相当于一个请求 来了100万个请求
}
}()
for { //阻塞主程序结束
fmt.Println("runtime.NumGoroutine() :", runtime.NumGoroutine())
time.Sleep(2 * time.Second)
}
}
参考:
http://marcio.io/2015/07/handling-1-million-requests-per-minute-with-golang/
注意到:
我们提供了初始化并加入到池子的worker的最大数量。
因为这个工程我们利用了Amazon Elasticbeanstalk带有的docker化的Go环境,所以我们常常会遵守12-factor方法论来配置我们的生成环境中的系统,我们从环境变了读取这些值。
这种方式,我们控制worker的数量和JobQueue的大小,所以我们可以很快的改变这些值,而不需要重新部署集群。
var (
MaxWorker = os.Getenv("MAX_WORKERS")
MaxQueue = os.Getenv("MAX_QUEUE")
)
结果:
我们部署了之后,立马看到了延时降到微乎其微的数值,并未我们处理请求的能力提升很大。Elastic Load Balancers完全启动后,我们看到ElasticBeanstalk 应用服务于每分钟1百万请求。
通常情况下在上午时间有几个小时,流量峰值超过每分钟一百万次。
我们一旦部署了新的代码,服务器的数量从100台大幅 下降到大约20台。
我们合理配置了我们的集群和自动均衡配置之后,我们可以把服务器的数量降至4x EC2 c4.Large实例,并且Elastic Auto-Scaling设置为如果CPU达到5分钟的90%利用率,我们就会产生新的实例。