秒懂 Golang 中的 条件变量(sync.Cond)
首先理解以下三点再进入正题:
- Go中的一个协程 可以理解成一个独立的人,多个协程是多个独立的人
- 多个协程都需要访问的 共享资源(比如共享变量) 可以理解成 多人要用的某种公共社会资源
- 上锁 其实就是加入到某个共享资源的争抢组中;上锁完成 就是从争抢组中被选出,得到了期待的共享资源;解锁 就是退出某个共享资源的争抢组。
假如有这样一个现实场景:在一个公园中有一个公共厕所,这个厕所一次只能容纳一个人上厕所,同时这个厕所中有个放卷纸的位置,其一次只能放一卷纸,一卷纸的总长度是 5 米,而每个人上一次厕所需要用掉 1 米的纸。而当一卷纸用完后,公园管理员要负责给厕所加上一卷新纸,以便大家可以继续使用厕所。 那么对于这个单人公共厕所,大家只能排队上厕所,当每个人进到厕所的时候,当然会把厕所门锁好,以便任何人都进不来(包括管理员)。管理员若要进到厕所查看用纸情况并加卷纸,也需要排队(因为插队总是不文明对吧)。
那么怎么用 Golang 去模拟上述场景呢?
首先我们先不用 sync.Cond,看如何实现?那么请看下面这段代码:
package main import ( "fmt" "time" "sync" ) var 卷纸 int var m sync.Mutex var wg sync.WaitGroup func 上厕所(姓名 string){ m.Lock() defer func(){ m.Unlock() wg.Done() }() fmt.Printf("%s 进到厕所t",姓名) if 卷纸 >= 1 { // 进到厕所第一件事是看还有没有纸 fmt.Printf("正在拉屎中...n") time.Sleep(time.Second) 卷纸 -= 1 fmt.Printf("%s 已用完厕所,正在离开n",姓名) return } fmt.Printf("发现纸用完了,无奈先离开厕所n") } func 加厕纸(){ m.Lock() defer func(){ m.Unlock() wg.Done() }() fmt.Printf("公园管理员 进到厕所t") if 卷纸
上面的代码已经能看出一些效果,但还是有问题:最后三个人因为厕纸用完,都直接离开厕所后就没有后续了?应该是他们离开厕所后再次尝试排队,直到需求解决,就离开厕所不再参与排队了,否则要不断去排队上厕所。而公园管理员呢,他要一直去排队进到厕所里看还有没有纸,而不是看一次就再也不管了。 那么请看下面的完善代码:
package main import ( "fmt" "sync" "time" ) var ( 卷纸 int m sync.Mutex wg sync.WaitGroup 厕所的排队 chan string ) func 上厕所(姓名 string) { m.Lock() // 该语句的调用只说明本执行体(可理解成该姓名所指的那个人)加入到了厕所资源的争抢组中; // 而该语句的完成调用,才代表了从争抢组中脱颖而出,抢到了厕所;在完成调用之前,会一直阻塞在这里(可理解为这个人正在争抢中) defer func() { m.Unlock() wg.Done() }() fmt.Printf("%s 进到厕所t", 姓名) if 卷纸 >= 1 { // 进到厕所第一件事是看还有没有纸 fmt.Printf("正在拉屎中...n") time.Sleep(time.Second) 卷纸 -= 1 fmt.Printf("%s 已用完厕所,正在离开n", 姓名) return } fmt.Printf("发现纸用完了,无奈先离开厕所n") 厕所的排队
上面这个代码在功能上基本是完善了,成功模拟了上述 多人上公厕 的场景。但仔细一想,这个场景其实有些地方是不合常理的:如果有个人进到厕所发现没纸,难道他会出来紧接着再去排队吗?如果排了三次五次甚至十次还是没有纸,还要这样不断地反复排队进去出来又排队?而公园管理员,要是这样不断反复排队进厕所查看,那么他这一天其他啥事都干不了。
所以更合理实际的情况应该是:如果一个人进到厕所发现没纸,他应该先去在旁边歇着或在附近干别的,当公园管理员加完纸后,会通过喇叭吆喝一声:“新纸已加上”。这样,附近所有因为没厕纸而歇着的人就会听到这个通知,此时,他们再去尝试排队进厕所;而公园管理员也不用不断去排队进厕所检查纸用完了没有,因为经过升级,厕所加装了一个功能,有一个纸用尽的报警按钮装在纸盒旁边,当上完厕所的人发现纸用完的时候,他会先按下这个报警按钮,再离开厕所。这个报警的声音在整个公园的各处都可以听到,所以管理员无论在哪里干啥,他都能收到这个纸用尽的报警信号,然后他才去进厕所加纸。
其实这种被动通知的模式就是 sync.Cond 的核心思想,它会减少资源消耗,达到更优的效果,下面就是改良为 sync.Cond 的实现代码:
package main import ( "fmt" "math" "strconv" "sync" "time" ) var ( 卷纸 int m sync.Mutex cond = sync.NewCond(&m) ) func 上厕所(姓名 string) { m.Lock() // 该语句的调用只说明本执行体(可理解成该姓名所指的那个人)加入到了厕所资源的争抢组中; // 而该语句的完成调用,才代表了从争抢组中脱颖而出,抢到了厕所;在完成调用之前,会一直阻塞在这里(可理解为这个人正在争抢中) defer m.Unlock() fmt.Printf("%s 进到厕所t", 姓名) for 卷纸 0 { // 管理员进到厕所是看纸有没有用完 fmt.Printf("发现纸还没用完,先离开厕所在等纸用尽的报警消息n") cond.Wait() // 如果纸没用完,就先去干其他工作,等纸用尽的报警消息 fmt.Printf("公园管理员 等到了纸用尽的报警消息,并再次抢到了厕所n") } fmt.Printf("公园管理员 正在加新纸...n") time.Sleep(time.Millisecond * 500) 卷纸 = 5 cond.Broadcast() // 注意:公园管理员加完新纸后,要通过喇叭喊一声 “纸已加上” 的消息通知所有 因没纸而等待上厕所的人 fmt.Printf("公园管理员 已加上新厕纸,并通过喇叭通知了该消息,并正在离开厕所n") } func main() { 卷纸 = 5 // 厕所一开始就准备好了一卷纸,长度5米 要上厕所的人 := [...]string{"老王", "小李", "老张", "小刘", "阿明", "欣欣", "西西", "芳芳"} // 上厕所的人名模板 go func() { // 在这个执行体中,代表厕所及厕所队列的时间线,厕所永远运营下去 for i := 0; i > 屏幕停止输出后,请按Enter键继续 > 屏幕停止输出后,请按Enter键继续 > 屏幕停止输出后,请按Enter键继续
用了 sync.Cond 的代码显然要精简了很多,而且还节省了计算资源,只会在收到通知的时候 才去抢公共厕所,而不是不断地反复去抢公共厕所。通过这个对现实场景的模拟,我们就很容易从使用者的角度理解 sync.Cond 是什么,它的字面意思就是 “条件”,这就已经点出了这东西的核心要义,就是满足条件才执行,条件是什么,信号其实就是条件,当一个执行体收到信号之后,它才去争抢共享资源,否则就会挂起等待(这种等待底层其实会让出线程,所以这种等待并不会空耗资源),比起不断轮寻去抢资源,这种方式要节省得多。
最后留给读者一个思考的问题:就是上面最后一版的代码,为什么 当纸用完后按报警按钮通知 公园管理员 要用 sync.Broadcast() 方法去广播通知?不是只通知管理员一个人吗,单独通知他不就行了,用 sync.Signal() 为什么不行?
文章来源于互联网:秒懂 Golang 中的 条件变量(sync.Cond)