go并发控制

并发控制
Golang基础库中已经提供不少并发控制工具,比如Channel、WaitGroup、各种锁等等。
 
ErrGroup
WaitGroup可以等待多个Goroutine执行结束,但很多时候并发执行多个任务,如果其中一个任务出错那么整体失败,需要直接返回,这种情况下我们可以使用ErrGroup
 
ErrGroup借助封装了WaitGroup、Once以及Context,调用Wait时如果一个任务失败取消Context直接返回,核心逻辑如下
 
type ErrGroup struct {
 ctx context.Context
 cancel func()
 
 wg sync.WaitGroup
 
 errOnce sync.Once
 err error
}
 
func (g *ErrGroup) Wait() error {
 g.wg.Wait()
 
 if g.cancel != nil {
  g.cancel()
 }
 
 return g.err
}
 
func (g *ErrGroup) Go(f func(ctx context.Context) error) {
 g.wg.Add(1)
 
 go func() {
  defer g.wg.Done()
  if err := f(g.ctx); err != nil {
            // 执行失败则运行cancel
   g.errOnce.Do(func() {
    g.err = err
    if g.cancel != nil {
     g.cancel()
    }
   })
  }
 }()
}
控制并发数
借助有缓冲的Channel,可以实现控制Goroutine并发数,逻辑如下:
 
func NewCtrlGroup(number int) *CtrlGroup {
 return &CtrlGroup{
  ch: make(chan struct{}, number),
 }
}
 
type CtrlGroup struct {
 ch chan struct{}
 wg sync.WaitGroup
}
 
func (g *CtrlGroup) Enter() {
 g.ch <- struct{}{}
}
 
func (g *CtrlGroup) Leave() {
 <-g.ch
}
 
func (g *CtrlGroup) Go(f func()) {
 g.Enter() // 接收到新任务,发送到Channel,如果Channel满需要等待
 g.wg.Add(1)
 
 go func() {
  defer g.Leave() // 任务结束,取出一个元素
  defer g.wg.Done()
  f()
 }()
}
 
func (g *CtrlGroup) Wait() {
 g.wg.Wait()
}
MapReduce
除了WaitGroup、ErrGroup处理一些简单的并发任务,有时候我们需要执行类似MapReduce的操作,通过Map对数据源并行处理,然后通过Reduce合并结果。在Java、Python中提供了类似功能。
 
比如实现一个实现一组数据的平方和,利用MapReduce在Golang中实现如下:
 
 num := 1000000
 
 res, err := mapreduce.New(mapreduce.WithWorkers(16)).
  From(func(r mapreduce.Writer) error { // 产生数据源
   for i := 1; i < num; i++ {
    r.Write(i)
   }
   return nil
  }).
  Map(func(item any) (any, error) { // 处理数据
   v, ok := item.(int)
   if !ok {
    return nil, fmt.Errorf("invaild type")
   }
 
   resp := v * v
 
   return resp, nil
  }).
  Reduce(func(r mapreduce.Reader) (any, error) { // 合并结果
   sum := 0
   for {
    item, ok := r.Read()
    if !ok {
     break
    }
 
    v, ok := item.(int)
    if !ok {
     return nil, fmt.Errorf("invaild type")
    }
 
    sum += v
   }
   return sum, nil
  }).
  Do()
主要逻辑是利用Channel(或者线程安全的队列)将源数据发送到Map的执行Worker中,处理完后再转发到Reduce Goroutine中,通过ErrGroup等待所有Worker执行完成。源码见mapreduce.go。
 
类似的也可以实现Kubernetes中Ctroller模式,通过队列或者Channel将生产者与消费者解耦,并行处理提高运行速度。
 
总结
本文总结了Golang的一些有趣的编程模式,例如链式调用、可选配置、并发控制等,通过这些技巧或者手段,可以提高编码的质量,所有代码见gocorex
posted @ 2022-12-06 13:09  技术颜良  阅读(159)  评论(0编辑  收藏  举报