golang实现等待通知机制的方法
参考博客:
https://geektutu.com/post/hpg-sync-cond.html
https://cyent.github.io/golang/goroutine/sync_cond/
使用sync.Cond实现
如果想在使用 Cond 的时候避免犯错,只要时刻记住**调用 cond.Wait 方法之前一定要加锁**,以及**waiter goroutine 被唤醒不等于等待条件被满足**这两个知识点。
package awaitnotify import ( "fmt" "math/rand" "sync" "testing" "time" ) func init() { rand.Seed(time.Now().UnixNano()) } // 使用Cond实现一个等待/通知机制 func TestWN1(t *testing.T) { c := sync.NewCond(&sync.Mutex{}) var ready int // 10名运动员 for i := 0; i < 10; i++ { go func(num int) { // 模拟运动员等待时间 time.Sleep(time.Duration(rand.Intn(5)) * time.Second) // 非原子操作加锁保护 c.L.Lock() ready++ c.L.Unlock() fmt.Printf("运动员 %d 已经就绪!\n", num) // 广播唤醒所有等待者 // c.Broadcast() // 唤醒其中一个等待者,因为只有一个等待者,所以用哪个方法都行 c.Signal() }(i) } // 1、调用Wait方法一定要持有 c.L 的锁 c.L.Lock() // 2、需要判断条件,否则一个子协裎就能让逻辑往下走 for ready != 10 { c.Wait() fmt.Println("裁判员被唤醒一次(有一个运动员已经准备好了)!") } c.L.Unlock() // 所有运动员准备就绪 fmt.Println("所有运动员准备就绪!开始比赛!") }
~~~
使用Waitgroup实现(纯等待的机制可以使用,没有自带通知机制)
本质上 WaitGroup 和 Cond 是有区别的:WaitGroup 是主 goroutine 等待确定数量的子 goroutine 完成任务;而 Cond 是等待某个条件满足,这个条件的修改可以被任意多的 goroutine 更新,而且 Cond 的 Wait 不关心也不知道其他 goroutine 的数量,只关心等待条件。而且 Cond 还有单个通知的机制,也就是 Signal 方法。
使用channel实现
清理正常
package main import ( "fmt" "os" "os/signal" "syscall" "time" ) func main() { var closing = make(chan struct{}) // 代表程序退出,但是清理工作还没做 var closed = make(chan struct{}) // 代表清理工作已经做完 go func() { // 模拟业务处理 for { select { case <-closing: fmt.Println("程序退出了.....") return default: // ....... 业务计算 time.Sleep(1 * time.Second) fmt.Println("业务计算........") } } }() // 处理CTRL+C等中断信号 // Notice 注意需要带缓存的Channel termChan := make(chan os.Signal, 1) signal.Notify(termChan, syscall.SIGINT, syscall.SIGTERM) <-termChan // 一直等着 close(closing) // 执行退出之前的清理动作 go doCleanup(closed) select { case <-closed: // 一直等着 case <-time.After(time.Second * 2): // 只等2秒,清理时间超过2秒就走这里 fmt.Println("清理超时,不等了") } fmt.Println("优雅退出") } func doCleanup(closed chan struct{}) { // Notice 清理的时间 time.Sleep((time.Second)) close(closed) fmt.Println("清理完毕!") }
清理超时
package main import ( "fmt" "os" "os/signal" "syscall" "time" ) func main() { var closing = make(chan struct{}) // 代表程序退出,但是清理工作还没做 var closed = make(chan struct{}) // 代表清理工作已经做完 go func() { // 模拟业务处理 for { select { case <-closing: fmt.Println("程序退出了.....") return default: // ....... 业务计算 time.Sleep(1 * time.Second) fmt.Println("业务计算........") } } }() // 处理CTRL+C等中断信号 // Notice 注意需要带缓存的Channel termChan := make(chan os.Signal, 1) signal.Notify(termChan, syscall.SIGINT, syscall.SIGTERM) <-termChan // 一直等着 close(closing) // 执行退出之前的清理动作 go doCleanup(closed) select { case <-closed: // 一直等着 case <-time.After(time.Second * 2): // 只等2秒,清理时间超过2秒就走这里 fmt.Println("清理超时,不等了") } fmt.Println("优雅退出") } func doCleanup(closed chan struct{}) { // Notice 清理的时间 time.Sleep((time.Minute)) close(closed) fmt.Println("清理完毕!") }
sync.Cond与channel的比较
绝大多数情况下我们使用channel就能实现需求,而使用Cond的话坑会比较多,
但是,还是有一批忠实的“粉丝”坚持在使用 Cond,原因在于 Cond 有三点特性是 Channel 无法替代的:
-
Cond 和一个 Locker 关联,可以利用这个 Locker 对相关的依赖条件更改提供保护。
-
Cond 可以同时支持 Signal 和 Broadcast 方法,而 Channel 只能同时支持其中一种。
这个是Cond存在的核心,结合 Cond 可以重用,才是下面k8s用Cond而不用Channel的原因 -
Cond 的 Broadcast 方法可以被重复调用。等待条件再次变成不满足的状态后,我们又可以调用 Broadcast 再次唤醒等待的 goroutine。这也是 Channel 不能支持的,Channel 被 close 掉了之后不支持再 open。
~~~