Go 循环栅栏

CyclicBarrier 循环栅栏

循环栅栏(CyclicBarrier),常常应用于重复进行一组 goroutine 同时执行的场景中。

CyclicBarrier (github.com/marusama/cyclicbarrier) 允许一组 goroutine 彼此等待,到达一个共同的执行点。同时,因为它可以被重复使用,所以叫循环栅栏。具体的机制是,大家都在栅栏前等待,等全部都到齐了,就抬起栅栏放行。

WaitGroup 更适合用在"一个 goroutine 等待一组 goroutine 到达同一个执行点"的场景中,或者是不需要重用的场景中。

CyclicBarrier 有两个初始化方法:

go 复制代码
func New(parties int) CyclicBarrier
func NewWithAction(parties int, barrierAction func() error) CyclicBarrier

New 它只需要一个参数,来指定循环栅栏参与者的数量;

NewWithAction 它额外提供一个函数,可以在每一次到达执行点的时候执行一次。具体的时间点是在最后一个参与者到达之后,但是其它的参与者还未被放行之前。我们可以利用它,做放行之前的一些共享状态的更新等操作。

CyclicBarrier 是一个接口,定义的方法如下:

scss 复制代码
type CyclicBarrier interface {
    // 等待所有的参与者到达,如果被ctx.Done()中断,会返回ErrBrokenBarrier
    Await(ctx context.Context) error
    // 重置循环栅栏到初始化状态。如果当前有等待者,那么它们会返回ErrBrokenBarrier
    Reset()
    // 返回当前等待者的数量
    GetNumberWaiting() int
    // 参与者的数量
    GetParties() int
    // 循环栅栏是否处于中断状态
    IsBroken() bool
}

循环栅栏的使用也很简单。循环栅栏的参与者只需调用 Await 等待,等所有的参与者都到达后,再执行下一步。当执行下一步的时候,循环栅栏的状态又恢复到初始的状态了,可以迎接下一轮同样多的参与者。

e.g.

有一个名叫大自然的搬运工的工厂,生产一种叫做一氧化二氢的神秘液体。这种液体的分子是由一个氧原子和两个氢原子组成的,也就是水。

这个工厂有多条生产线,每条生产线负责生产氧原子或者是氢原子,每条生产线由一个 goroutine 负责。

这些生产线会通过一个栅栏,只有一个氧原子生产线和两个氢原子生产线都准备好,才能生成出一个水分子,否则所有的生产线都会处于等待状态。也就是说,一个水分子必须由三个不同的生产线提供原子,而且水分子是一个一个按照顺序产生的,每生产一个水分子,就会打印出 HHO、HOH、OHH 三种形式的其中一种。HHH、OOH、OHO、HOO、OOO 都是不允许的。

生产线中氢原子的生产线为 2N 条,氧原子的生产线为 N 条。

如何实现呢?首先,我们来定义一个 H2O 辅助数据类型,它包含两个信号量的字段和一个循环栅栏。 semaH 信号量:控制氢原子。一个水分子需要两个氢原子,所以,氢原子的空槽数资源数设置为 2。 semaO 信号量:控制氧原子。一个水分子需要一个氧原子,所以资源数的空槽数设置为 1。 循环栅栏:等待两个氢原子和一个氧原子填补空槽,直到任务完成。

go 复制代码
package main

import (
 "context"
 "log"
 "math/rand"
 "sort"
 "sync"
 "time"

 "github.com/marusama/cyclicbarrier"
 "golang.org/x/sync/semaphore"
)

// 定义水分子合成的辅助数据结构
type H2O struct {
 semaH *semaphore.Weighted         // 氢原子的信号量
 semaO *semaphore.Weighted         // 氧原子的信号量
 b     cyclicbarrier.CyclicBarrier // 循环栅栏,用来控制合成
}

func New() *H2O {
 return &H2O{
  semaH: semaphore.NewWeighted(2), //氢原子需要两个
  semaO: semaphore.NewWeighted(1), // 氧原子需要一个
  b:     cyclicbarrier.New(3),     // 需要三个原子才能合成
 }
}

func (h2o *H2O) hydrogen(releaseHydrogen func()) {
 h2o.semaH.Acquire(context.Background(), 1)

 releaseHydrogen()                 // 输出H
 h2o.b.Await(context.Background()) //等待栅栏放行
 h2o.semaH.Release(1)              // 释放氢原子空槽
}

func (h2o *H2O) oxygen(releaseOxygen func()) {
 h2o.semaO.Acquire(context.Background(), 1)

 releaseOxygen()                   // 输出O
 h2o.b.Await(context.Background()) //等待栅栏放行
 h2o.semaO.Release(1)              // 释放氢原子空槽
}

func main() {
 //用来存放水分子结果的channel
 var ch chan string
 releaseHydrogen := func() {
  ch <- "H"
 }
 releaseOxygen := func() {
  ch <- "O"
 }

 // 300个原子,300个goroutine,每个goroutine并发的产生一个原子
 N := 100
 ch = make(chan string, N*3)

 h2o := New()

 // 用来等待所有的goroutine完成
 var wg sync.WaitGroup
 wg.Add(N * 3)

 // 200个氢原子goroutine
 for i := 0; i < 2*N; i++ {
  go func() {
   time.Sleep(time.Duration(rand.Intn(100)) * time.Millisecond)
   h2o.hydrogen(releaseHydrogen)
   wg.Done()
  }()
 }
 // 100个氧原子goroutine
 for i := 0; i < N; i++ {
  go func() {
   time.Sleep(time.Duration(rand.Intn(100)) * time.Millisecond)
   h2o.oxygen(releaseOxygen)
   wg.Done()
  }()
 }

 //等待所有的goroutine执行完
 wg.Wait()

 // 结果中肯定是300个原子
 if len(ch) != N*3 {
  log.Fatalf("expect %d atom but got %d", N*3, len(ch))
 }

 // 每三个原子一组,分别进行检查。要求这一组原子中必须包含两个氢原子和一个氧原子,这样才能正确组成一个水分子。
 var s = make([]string, 3)
 for i := 0; i < N; i++ {
  s[0] = <-ch
  s[1] = <-ch
  s[2] = <-ch
  sort.Strings(s)

  water := s[0] + s[1] + s[2]
  if water != "HHO" {
   log.Fatalf("expect a water molecule but got %s", water)
  }
 }
}

e.g.

go 复制代码
package main

import (
 "context"
 "fmt"
 "sync"
 "time"

 "github.com/marusama/cyclicbarrier"
)

func main() {
 // create a barrier for 10 parties with an action that increments counter
 // this action will be called each time when all goroutines reach the barrier
 cnt := 0
 b := cyclicbarrier.NewWithAction(10, func() error {
  cnt++
  return nil
 })

 wg := sync.WaitGroup{}
 for i := 0; i < 10; i++ { // create 10 goroutines (the same count as barrier parties)
  wg.Add(1)
  go func() {
   for j := 0; j < 5; j++ {

    // do some hard work 5 times
    time.Sleep(100 * time.Millisecond)

    err := b.Await(context.TODO()) // ..and wait for other parties on the barrier.
    // Last arrived goroutine will do the barrier action
    // and then pass all other goroutines to the next round
    if err != nil {
     panic(err)
    }
   }
   wg.Done()
  }()
 }

 wg.Wait()
 fmt.Println(cnt) // cnt = 5, it means that the barrier was passed 5 times
}
相关推荐
程序员老邢2 小时前
【产品底稿 07】商助慧 Admin 运维模块落地:从 “能跑” 到 “能运维”,3 个页面搞定日常排障
java·运维·经验分享·spring boot·后端
彩票管理中心秘书长2 小时前
npm 依赖管理机制完全解析(超详细版)
后端
彩票管理中心秘书长2 小时前
npm 脚本与自动化完全指南(超详细版)
后端
元宝骑士2 小时前
Spring @Async 异步无法获取当前登录用户?Sa-Token 1.34.0 终极踩坑解决方案
java·后端
鱼人2 小时前
Fibers(纤程)来了:打破阻塞,实现纯PHP下的异步非阻塞IO
后端
长大19882 小时前
生成器(Generators)与内存救赎:处理百万级数据导出的极简方案
后端
小强19882 小时前
构造函数属性提升的利与弊:如何优雅地编写价值对象(Value Object)
后端
彩票管理中心秘书长2 小时前
npm 基础认知与环境准备(超详细版)
后端
二月龙2 小时前
类型系统攻防战:PHP混合类型与联合类型对隐式类型转换漏洞的防御策略
后端