在go并发的时候,我们经常会遇到几个问题:
- goroutine的数量控制,创建过多,浪费资源。且并发效果也并非那么好。他正如正态分布那样。到达某个极点所带来的收益将会下降
- goroutine能否复用?
- goroutine之间高效的协同工作?
- 这个问题是否能并行处理,提升效率?
并发模式就是我们在遇到这些并发场景后,解决问题并提炼得到的通用模式。
下面我们来研究几种常用的模式:
waitgroup
Go语言自带的sync.WaitGroup可以帮助我们在创建一堆goroutine后,确保它们都执行完成。但是却不能控制goroutine的数量,如果遇到特殊情况,可能goroutine数量无限增长。
我们来优化一下它,让它可以限制一下goroutine数量,就很完美了。
type WaitGroup struct {
workChan chan int
wg sync.WaitGroup
}
func NewWaitGroup(coreNum int) *WaitGroup {
//创建一个coreNum长度的channel,来限制goroutine数量
ch := make(chan int, coreNum)
return &WaitGroup{
workChan: ch,
wg: sync.WaitGroup{},
}
}
func (ap *WaitGroup) Add(num int) {
for i := 0; i < num; i++ {
// 向channel写入一个值。如果channel缓冲满了,该步骤就会阻塞住。从而限制goroutine数量
ap.workChan <- i
ap.wg.Add(1)
}
}
// Done 完结
func (ap *WaitGroup) Done() {
LOOP:
for {
select {
case <-ap.workChan:
// 从channel读出一个值
break LOOP
}
}
ap.wg.Done()
}
// Wait 等待
func (ap *WaitGroup) Wait() {
ap.wg.Wait()
}
使用:
// 限制100个goroutine数量
var wg = NewWaitGroup(100)
for i := 0; i < 1000; i++ {
wg.Add(1)
go func(ii int) {
defer wg.Done()
//业务逻辑...
}(i)
}
wg.Wait()
worker pool
worker pool 与 waitgroup的区别是:
- worker pool 创建若干的goroutine,并对它们进行复用。
- waitgroup 不复用goroutine,每次都创建新的goroutine
work pool的精髓在于将任务,与groutine进行分离。只关心初始的任务与结果。
worker pool适用于长时间运行的任务。比如从kafka消费,然后投递给worker pool进行处理。
我们来实现一个可伸缩的worker pool 。
启动时创建一批goroutine,如果高峰期任务量超出goroutine数量,就创建若干临时goroutine。当任务量下降后,临时goroutine会自动销毁。
// ScalablePoolConfig 可伸缩协程池配置
type ScalablePoolConfig[T any] struct {
MinParallel int //初始goroutine数量
MaxParallel int //最大goroutine数量
TempDuration time.Duration //临时goroutine生命时长
OnDelivery func(job T) //处理任务
}
// ScalablePool 可伸缩协程池
type ScalablePool[T any] struct {
ctx context.Context
pool chan chan<- T //空闲goroutine的任务channel
conf *ScalablePoolConfig[T]
mutex sync.Mutex
rest chan int
}
func NewScalePool[T any](ctx context.Context, conf *ScalablePoolConfig[T]) *ScalablePool[T] {
restCount := conf.MaxParallel - conf.MinParallel
sp := &ScalablePool[T]{
ctx: ctx,
pool: make(chan chan<- T, conf.MaxParallel),
conf: conf,
rest: make(chan int, restCount),
}
//创建初始goroutine
for i := 0; i < conf.MinParallel; i++ {
sp.newWorker()
}
return sp
}
func (c *ScalablePool[T]) Delivery(job T) {
for {
select {
case ch := <-c.pool:
//从pool中取一个空闲goroutine对应的channel,向其发送任务
ch <- job
return
default:
//如果无空闲goroutine,就等待一个空闲goroutine,或者创建一个临时goroutine
select {
case ch := <-c.pool:
ch <- job
return
case c.rest <- 1:
//无空闲goroutine,且goroutine数量在最大范围内,就创建新的临时goroutine
c.newTempWorker()
}
}
}
}
func (c *ScalablePool[T]) newWorker() {
ch := make(chan T)
go func() {
for {
select {
case <-c.ctx.Done():
return
case m := <-ch:
c.conf.OnDelivery(m)
//处理完任务后,将channel放入pool中
//表示自己已经空闲,等待接收下一个任务
c.pool <- ch
}
}
}()
//将channel放入pool中,等待接收任务
c.pool <- ch
}
func (c *ScalablePool[T]) newTempWorker() {
ch := make(chan T)
go func() {
timeout := time.After(c.conf.TempDuration)
for {
select {
case <-c.ctx.Done():
return
case m := <-ch:
c.conf.OnDelivery(m)
select {
case <-timeout:
//如果已经超出生命时长,就退出,结束当前goroutine
<-c.rest
return
default:
//将channel放回pool中,等待接收任务
c.pool <- ch
}
}
}
}()
//将channel放入pool中,等待接收任务
c.pool <- ch
}
使用:
pool := NewScalePool(context.Background(), &ScalablePoolConfig[int]{
MinParallel: 500,
MaxParallel: 1000,
TempDuration: 2 * time.Second,
OnDelivery: func(n int) {
a := 0
for i := 0; i < 1000; i++ {
a += 1
}
},
})
for i := 0; i < 10000; i++ {
pool.Delivery(i)
}
<-time.After(10 * time.Second)
流式编程
以上图为例,输入一串数字(2,2,3,4,5),经过Filter、Sort、Distinct三道工序,将最终结果存入数据库。
一个流的数据处理存在多个处理单元,每个处理单元接收上游Stream的数据,并将处理后的数据以一个新的Stream发送给下游。
可以想象是流水线上的4个工人,4个工人同时工作,每个工人都在不停从传送带上取出零件加工,加工完又放回传送带,交给下一个工人加工。
代码演示:
type Stream[T any] struct {
source <-chan T
}
func NewStream[T any](f func(source chan<- T)) Stream[T] {
source := make(chan T)
go func() {
defer close(source)
f(source)
}()
return Stream[T]{source: source}
}
func (s Stream[T]) Filter(f func(item T) bool) Stream[T] {
return s.Work(func(item T, pipe chan<- T) {
if f(item) {
pipe <- item
}
})
}
func (s Stream[T]) Sort(less func(a, b T) bool) Stream[T] {
items := make([]T, 0)
for item := range s.source {
items = append(items, item)
}
sort.Slice(items, func(i, j int) bool {
return less(items[i], items[j])
})
pipe := make(chan T, len(items))
for _, item := range items {
pipe <- item
}
close(pipe)
return Stream[T]{source: pipe}
}
func (s *Stream[T]) Work(f func(item T, pipe chan<- T)) Stream[T] {
pipe := make(chan T)
go func() {
var wg sync.WaitGroup
for item := range s.source {
wg.Add(1)
go func(val T) {
defer wg.Done()
f(val, pipe)
}(item)
}
wg.Wait()
close(pipe)
}()
return Stream[T]{source: pipe}
}
func (s Stream[T]) Show() {
for item := range s.source {
fmt.Println(item)
}
}
func main() {
NewStream[int](func(source chan<- int) {
for _, i := range []int{6, 2, 4, 3, 5} {
source <- i
}
}).Filter(func(item int) bool {
return item > 2
}).Sort(func(a, b int) bool {
return a > b
}).Show()
}
这里是简单演示流处理的流程。 感兴趣的可以看看go-zero的流处理工具fx
map reduce
map reduce是把一个复杂任务分解为若干个“简单任务”来并行处理。
“简单任务”的要求:
1.是数据或计算的规模相对原任务要缩小
2.这些简单任务可以并行计算,彼此间几乎没有依赖关系。
map部分是拆分小任务,并且并行执行
reduce部分是对map阶段的结果进行汇总
下面我们简单实现一个map reduce。对一串数字{1, 2, 3, 4, 5},分别平方后求和。
type (
GenerateFunc[T any] func(source chan<- T)
MapperFunc[T any] func(item T, writer chan<- T)
ReducerFunc[T any] func(pipe <-chan T, writer chan<- any)
)
func MapReduce[T any](generate GenerateFunc[T], mapper MapperFunc[T], reducer ReducerFunc[T]) any {
var wg sync.WaitGroup
source := make(chan T)
wg.Add(1)
go func() {
defer wg.Done()
defer close(source)
generate(source)
}()
handle := make(chan T, 5)
wg.Add(1)
go func() {
defer wg.Done()
// 开启5个goroutine,同时进行mapper
var wg2 sync.WaitGroup
for i := 0; i < 5; i++ {
wg2.Add(1)
go func() {
defer wg2.Done()
for it := range source {
mapper(it, handle)
}
}()
}
wg2.Wait()
close(handle)
}()
done := make(chan any, 1)
wg.Add(1)
go func() {
defer wg.Done()
reducer(handle, done)
}()
wg.Wait()
select {
case out := <-done:
return out
default:
return nil
}
}
func main() {
rsp := MapReduce[int](
func(source chan<- int) {
for _, i := range []int{1, 2, 3, 4, 5} {
source <- i
}
},
func(item int, writer chan<- int) {
writer <- item * item
},
func(pipe <-chan int, writer chan<- any) {
var r int
for i := range pipe {
r += i
}
writer <- r
},
)
fmt.Println("结果", rsp)
}