| 导语 作为一个后台开发,经常会面对处理大规模并发请求的问题。个人使用Go开发也有段时间了,最近写了个带优先级的异步并发工作池模块(workpool),就异步并发、协程管道、工作池这块相关的问题和一些思考及解决方案做一下分享,欢迎大家来喷。

1. 写在前面

作为一个后台开发,经常会面对处理大规模并发请求的问题。也有很多情况,为了提升服务的性能使用异步处理机制。Golang则从语言层面上提供了并发支持,Goroutine和channel的使用可以高效的利用多核cpu,也使并发编程变得容易许多。关于Go的并发模型可以参考golang CSP并发模型

个人使用Go开发也有段时间了,最近写了个带优先级的异步并发工作池模块(workpool),就异步并发、协程管道、工作池这块相关的问题和一些思考及解决方案做一下分享,欢迎大家来喷。

突出特点:

异步执行;

高并发;

支持任务优先级;

使用极其简单;

只需要导入workpool包,实现具体的Job(只有一个Do()接口)然后执行workpool.SubmitJob(job, 优先级)即可。


2. 常见问题场景及思考

2.1 大规模请求或任务执行

对于高并发的业务场景这种情况再常见不过了,如果串行执行这些任务,显然无法体现Golang的并发优势。通常这些个请求都是无状态的或者说相互独立的,并发的去执行才是正解。

为每个任务都创建一个Goroutine去并发执行?

虽说Goroutine非常轻量级,可以同时创建成千上万个协程执行,但是过多的协程并不能有效的提高程序的处理效率,goroutine只是一个并发的任务单元,并不是并发的线程,巨量的goroutine反而会占用大量资源。

golang中有没有类似C/C++中线程池的结构,比如协程池?

目前来看golang官方并没有协程池这个东西,不过可以利用带缓冲的channel来封装实现。

2.2 异步非阻塞

对于一些请求或任务(比如:日志的打点,状态的更新)并不需要立即知道处理的结果,这种情况下使用异步处理的机制是非常合理的,可以大大优化服务器的处理性能。Golang中channel可以设置缓存区的,在数据没有写满缓冲区时,程序是不会阻塞的。

如何有效的使用Channel?

缓冲区设置太小的话很快会被阻塞,缓冲区设置过大很多任务会长时间积累在channel中得不到处理还浪费资源。其实Goroutine和channel就是一对好搭档,他们俩组合一块可以解决很多问题。可以设置稍大一点的channel缓冲区然后用一定数目的goroutine去消费。

2.3 任务优先级

在许多业务场景中对于不同的请求或是不同的任务都有其不同的重要程度,比如线上接口可以分为核心接口、重要接口、和普通接口,日志也有重要,普通,非重要等。那么我们需要更具任务的重要程度设定优先级,然后根据优先级来做不同的响应处理。在golang中可以使用channel做job队列,那么问题来了:

如何实现channel的优先级呢?

golang原生并没有对channel做优先级的支持,而使用select语句也是随机选择一个可用的channel执行。要实现管道的优先级需要一点tricky的技巧。可以组合优先级筛选函数和select嵌套实现优先级管道,详情见第4部分实现。


3. 方案设计

根据以上描述的场景问题,针对优先级job队列,异步,协程池分别设计对应的方案。

使用channel封装一个可以表示优先级的job队列,然后采用加权随机算法来决定优先级;

// PriorityChan 封装了三个channel类型,分别表示高、中、低三种// 类型的管道type PriorityChan struct {
HeighChan chan interface{}
MiddleChan chan interface{}
LowChan chan interface{}
}

使用Master/worker模式以及二级管道实现workpool

每个worker都封装有一个传递job的管道,并且以goroutine的形式运行;

woker运行是不断从自身job管道中读取job来执行任务;

worker在运行前会将自身的job管道写进全局的workpool管道(这个管道是一个二级嵌套管道,它传输的内容是woker的传输job的管道,即传输管道的管道);

master负责从全局优先级管道中取出job,然后分发个可用的worker。(worker中的job管道是无缓冲的,如果没有来得及被取走就会阻塞在那,此时worker不可用);

下图是一个job的在工作池中的异步处理流程:

4. 代码实现

代码分三部分实现:

一、优先级管道部分。

主要实现封装在priority_chan.go文件中,使用三个interface类型的管道封装到一个struct中作为实现优先级管道的基础结构。

优先级的体现用一个比较简单的“加权随机算法”来做具体实现。

package workpool
 
import (
"math/rand"
"sync"
"time"
)
 
const (
HEIGH = 3
MIDDLE = 2
LOW = 1
)
 
var (
pChan *PriorityChan
once sync.Once
Buffsize int32 = 100
 
//LevelArr 取合适优先级channel时用到的加权随机扩展集合
LevelArr = [...]int{HEIGH, HEIGH, HEIGH, HEIGH, HEIGH, HEIGH,
MIDDLE, MIDDLE, MIDDLE, LOW}
)
 
// PriorityChan 封装了三个channel类型,分别表示高、中、低三种// 类型的管道
type PriorityChan struct {
HeighChan chan interface{}
MiddleChan chan interface{}
LowChan chan interface{}
}
 
// CreatePriorityChan 创建一个优先级管道的单例
func CreatePriorityChan() *PriorityChan {
once.Do(func() {
pChan = &PriorityChan{
HeighChan: make(chan interface{}, Buffsize*3),
MiddleChan: make(chan interface{}, Buffsize*2),
LowChan: make(chan interface{}, Buffsize),
}
})
 
return pChan
}
 
func init() {
pChan = CreatePriorityChan()
}
 
// GetLevel 加权随机算法,获取一个级别// heigh:midlle:low的比例= 6:3:1
func (this *PriorityChan) getLevel() int {
rand.Seed(time.Now().Unix())
index := rand.Intn(10)
 
return LevelArr[index]
}
 
// GetChan 根据优先级获取一个channel
func (this *PriorityChan) GetChan() chan interface{} {
l := this.getLevel()
switch l {
case HEIGH:
return pChan.HeighChan
case MIDDLE:
return pChan.MiddleChan
case LOW:
return pChan.LowChan
}
 
return nil
}

二、异步工作池的主体实现部分。

这部分在在workpool.go文件中,为Master/worker模式的具体实现。

package workpool
 
import (
"fmt"
)
 
var (
// PoolSize 工作池的容量,即goroutine的并发数
PoolSize = 20
WorkPool WorkChannel
 
master *Master
)
 
func init() {
WorkPool = make(WorkChannel, PoolSize)
//once.Do(InitMaster)
InitMaster()
master.RunWorkers()
}
 
// Job 工作任务接口,具体内容由子struct实现
type Job interface {
Do() error
}
 
// JobChannel 定义一个传输Job的管道类型
type JobChannel chan Job
 
// WorkChannel 定义一个工作池的管道类型,传输的内容是传输job的管道// 充分利用了带缓冲区管道的特点,来实现池子的功能
type WorkChannel chan JobChannel
 
// Worker 从自身job管道里读取job,并执行job
type Worker struct {
JobChan JobChannel
quit chan bool
}
 
func NewWorker() *Worker {
// JobChannel 管道成员为无缓冲区模式,阻塞在一个job上,达到占用
// 这个worker的目的
return &Worker{make(JobChannel), make(chan bool)}
}
 
func (this *Worker) Start() {
go func() {
for {
// 将自身的job管道写入workpool管道中待取
WorkPool <- this.JobChan
select {
case job := <-this.JobChan:
if err := job.Do(); err != nil {
fmt.Printf("job failed with err: %v", err)
}
case <-this.quit:
return
}
}
}()
}
 
func (this *Worker) Stop() {
this.quit <- true
close(this.JobChan)
}
 
// Master 定义一个异步工作池的管理者,负责启停worker单元,// 还有根据job优先级分发job到具体worker.
type Master struct {
workers []*Worker
quit chan bool
}
 
// CreateMaster 创建Master的单例func InitMaster() {
master = &Master{make([]*Worker, 0), make(chan bool)}
}
 
// RunWorkers 启动worker单元,分发job
func (this *Master) RunWorkers() {
for i := 0; i < PoolSize; i++ {
w := NewWorker()
w.Start()
this.workers = append(this.workers, w)
}
 
go this.dispatchJob()
}
 
// dispatchJob 分发全局优先级管道中的job到可用的worker中
func (this *Master) dispatchJob() {
for {
select {
case job := <-pChan.GetChan():
go this.putJob(job.(Job))
default:
select {
case <-this.quit:
return
default:
}
}
}
}
 
// putJob 将job放入到workpool中的一个管道中
func (this *Master) putJob(job Job) {
jobChan := <-WorkPool
jobChan <- job
 
}
 
func (this *Master) Stop() {
this.quit <- true
 
for _, w := range this.workers {
w.Stop()
}
 
this.workers = nil
}

三、包装对外接口

此部分在wrapper.go文件中,只有一个提供给对外调用的函数SubmitJob来提交任务,提交的任务优先级可选,默认是中等优先级。

package workpool
 
func SubmitJob(job Job, levels ...int) {
l := MIDDLE
 
if len(levels) > 0 {
if levels[0] >= HEIGH {
l = HEIGH
} else if levels[0] <= LOW {
l = LOW
}
}
 
switch l {
case HEIGH:
pChan.HeighChan <- job
case MIDDLE:
pChan.MiddleChan <- job
case LOW:
pChan.LowChan <- job
default:
}
}

5. 测试样例

为了模拟优先级,我这用sleep不同时间表示不同任务的处理耗时, 这里优先级暂时按照处理时常来定,处理耗时长的属于高优先级任务,短的表示低优先级任务。下面看具体代码:

package main
 
import (
"fmt"
"time"
"workpool"
)
 
type DemoJob struct {
cost time.Duration
id int
}
 
func (this *DemoJob) Do() error {
fmt.Printf("Doing job[id:%d] cost: %v\n", this.id, this.cost)
// 模拟任务处理耗时
time.Sleep(this.cost)
 
return nil
}
 
func main() {
 
go func() {
i := 1
for {
job := &DemoJob{400 * time.Millisecond, i}
// 提交任务到带优先级的异步工作池,优先级参数可选
workpool.SubmitJob(job, workpool.LOW)
i++
time.Sleep(400 * time.Millisecond)
}
}()
 
go func() {
i := 1
for {
job := &DemoJob{800 * time.Millisecond, i}
workpool.SubmitJob(job, workpool.HEIGH)
i++
time.Sleep(400 * time.Millisecond)
}
}()
 
select {}
}
 
 
 
#################################################
package workpool
 
import (
    "math/rand"
    "sync"
    "time"
)
 
const (
    HEIGH  = 3
    MIDDLE = 2
    LOW    = 1
)
 
var (
    pChan    *PriorityChan
    once     sync.Once
    Buffsize int32 = 100
 
    //LevelArr 取合适优先级channel时用到的加权随机扩展集合
    LevelArr = [...]int{HEIGH, HEIGH, HEIGH, HEIGH, HEIGH, HEIGH,
        MIDDLE, MIDDLE, MIDDLE, LOW}
)
 
// PriorityChan 封装了三个channel类型,分别表示高、中、低三种
// 类型的管道
type PriorityChan struct {
    HeighChan  chan interface{}
    MiddleChan chan interface{}
    LowChan    chan interface{}
}
 
// CreatePriorityChan 创建一个优先级管道的单例
func CreatePriorityChan() *PriorityChan {
    once.Do(func() {
        pChan = &PriorityChan{
            HeighChan:  make(chan interface{}, Buffsize*3),
            MiddleChan: make(chan interface{}, Buffsize*2),
            LowChan:    make(chan interface{}, Buffsize),
        }
    })
 
    return pChan
}
 
func init() {
    pChan = CreatePriorityChan()
}
 
// GetLevel 加权随机算法,获取一个级别
// heigh:midlle:low的比例= 6:3:1
func (this *PriorityChan) getLevel() int {
    rand.Seed(time.Now().Unix())
    index := rand.Intn(10)
 
    return LevelArr[index]
}
 
// GetChan 根据优先级获取一个channel
func (this *PriorityChan) GetChan() chan interface{} {
    l := this.getLevel()
    switch l {
    case HEIGH:
        return pChan.HeighChan
    case MIDDLE:
        return pChan.MiddleChan
    case LOW:
        return pChan.LowChan
    }
 
    return nil
}
 
import (
    "fmt"
)
 
var (
    // PoolSize 工作池的容量,即goroutine的并发数
    PoolSize = 20
    WorkPool WorkChannel
 
    master *Master
)
 
func init() {
    WorkPool = make(WorkChannel, PoolSize)
    //once.Do(InitMaster)
    InitMaster()
    master.RunWorkers()
}
 
// Job 工作任务接口,具体内容由子struct实现
type Job interface {
    Do() error
}
 
// JobChannel 定义一个传输Job的管道类型
type JobChannel chan Job
 
// WorkChannel 定义一个工作池的管道类型,传输的内容是传输job的管道
// 充分利用了带缓冲区管道的特点,来实现池子的功能
type WorkChannel chan JobChannel
 
// Worker 从自身job管道里读取job,并执行job
type Worker struct {
    JobChan JobChannel
    quit    chan bool
}
 
func NewWorker() *Worker {
    // JobChannel 管道成员为无缓冲区模式,阻塞在一个job上,达到占用
    // 这个worker的目的
    return &Worker{make(JobChannel), make(chan bool)}
}
 
func (this *Worker) Start() {
    go func() {
        for {
            // 将自身的job管道写入workpool管道中待取
            WorkPool <- this.JobChan
            select {
            case job := <-this.JobChan:
                if err := job.Do(); err != nil {
                    fmt.Printf("job failed with err: %v", err)
                }
            case <-this.quit:
                return
            }
        }
    }()
}
 
func (this *Worker) Stop() {
    this.quit <- true
    close(this.JobChan)
}
 
// Master 定义一个异步工作池的管理者,负责启停worker单元,
// 还有根据job优先级分发job到具体worker.
type Master struct {
    workers []*Worker
    quit    chan bool
}
 
// CreateMaster 创建Master的单例
func InitMaster() {
    master = &Master{make([]*Worker, 0), make(chan bool)}
}
 
// RunWorkers 启动worker单元,分发job
func (this *Master) RunWorkers() {
    for i := 0; i < PoolSize; i++ {
        w := NewWorker()
        w.Start()
        this.workers = append(this.workers, w)
    }
 
    go this.dispatchJob()
}
 
// dispatchJob 分发全局优先级管道中的job到可用的worker中
func (this *Master) dispatchJob() {
    for {
        select {
        case job := <-pChan.GetChan():
            go this.putJob(job.(Job))
        default:
            select {
            case <-this.quit:
                return
            default:
            }
        }
    }
}
 
// putJob 将job放入到workpool中的一个管道中
func (this *Master) putJob(job Job) {
    jobChan := <-WorkPool
    jobChan <- job
 
}
 
func (this *Master) Stop() {
    this.quit <- true
 
    for _, w := range this.workers {
        w.Stop()
    }
 
    this.workers = nil
}
 
func SubmitJob(job Job, levels ...int) {
    l := MIDDLE
 
    if len(levels) > 0 {
        if levels[0] >= HEIGH {
            l = HEIGH
        } else if levels[0] <= LOW {
            l = LOW
        }
    }
 
    switch l {
    case HEIGH:
        pChan.HeighChan <- job
    case MIDDLE:
        pChan.MiddleChan <- job
    case LOW:
        pChan.LowChan <- job
    default:
    }
}