Go语言高并发系列四:并发模式

346 阅读2分钟

在go并发的时候,我们经常会遇到几个问题:

  1. goroutine的数量控制,创建过多,浪费资源。且并发效果也并非那么好。他正如正态分布那样。到达某个极点所带来的收益将会下降
  2. goroutine能否复用?
  3. goroutine之间高效的协同工作?
  4. 这个问题是否能并行处理,提升效率?

并发模式就是我们在遇到这些并发场景后,解决问题并提炼得到的通用模式。

下面我们来研究几种常用的模式:

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)

流式编程

image.png 以上图为例,输入一串数字(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阶段的结果进行汇总

image.png

下面我们简单实现一个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)
}